feat: enhance benchmark and add surface graph
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Fri, 20 Mar 2026 21:03:36 +0000 (23:03 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Fri, 20 Mar 2026 21:03:36 +0000 (23:03 +0200)
- Add results dialog with copy-to-clipboard
- Add lit solid cubes test
- Update benchmark results and documentation
- Add SurfaceGraph3D class for 3D surface visualization
- Split and rename GraphDemo to SineHeightmap
- Clarify 'Cores' label as 'CPU cores'

doc/Screenshots/Benchmark.png [new file with mode: 0644]
doc/index.org
src/main/java/eu/svjatoslav/sixth/e3d/examples/SineHeightmap.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/BenchmarkTest.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/GraphicsBenchmark.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/LitSolidCubesTest.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/TexturedCubesTest.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/WireframeCubesTest.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/graph_demo/MathGraphsDemo.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/graph_demo/SurfaceGraph3D.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/launcher/ApplicationListPanel.java

diff --git a/doc/Screenshots/Benchmark.png b/doc/Screenshots/Benchmark.png
new file mode 100644 (file)
index 0000000..5029be0
Binary files /dev/null and b/doc/Screenshots/Benchmark.png differ
index f76fea1..807bb65 100644 (file)
@@ -39,7 +39,7 @@
 #+attr_latex: :width 1000px
 [[file:overview.png]]
 
-Goal of this project is to show off capabilities and API usage of
+The goal of this project is to show off capabilities and API usage of
 [[https://www3.svjatoslav.eu/projects/sixth-3d/][Sixth 3D]] engine.
 
 All [[id:5f88b493-6ab3-4659-8280-803f75dbd5e0][example scenes in this repository]] render at interactive
@@ -50,7 +50,7 @@ from here: [[file:sixth-3d-demos.jar]]
 
 It requires Java 21 or newer to run.
 
-To start demo application, use command:
+To start the demo application, use command:
 : java -jar sixth-3d-demos.jar
 
 * Navigating in space
@@ -71,6 +71,10 @@ To start demo application, use command:
 :ID:       5f88b493-6ab3-4659-8280-803f75dbd5e0
 :END:
 
+Press *F12* in any demo to open the [[https://www3.svjatoslav.eu/projects/sixth-3d/#outline-container-developer-tools][Developer Tools panel]]. This
+debugging interface provides real-time insight into the rendering
+pipeline with diagnostic toggles.
+
 ** Conway's Game of Life
 :PROPERTIES:
 :CUSTOM_ID: conways-game-of-life
@@ -99,7 +103,7 @@ Usage:
 |--------------------------------+--------------------------------------|
 | mouse click on the cell (cell) | toggles cell state                   |
 | <space>                        | next iteration                       |
-| ENTER                          | next iteeration with the history     |
+| ENTER                          | next iteration with the history     |
 | "c"                            | clear the matrix                     |
 
 ** Text editors
@@ -112,7 +116,7 @@ Usage:
 
 Initial test for creating user interfaces in 3D and:
 + window focus handling
-+ picking objecs using mouse
++ picking objects using mouse
 + redirecting keyboard input to focused window
 
 
@@ -153,7 +157,7 @@ again, window must be unfocused first using ESC key.
 
 See also [[https://hackers-1995.vercel.app/][similar looking web based demo]] ! :)
 
-** Mathematical formulas
+** Math graphs demo
 :PROPERTIES:
 :CUSTOM_ID: mathematical-formulas
 :ID:       b1c2d3e4-f5a6-7890-bcde-f12345678901
@@ -162,7 +166,7 @@ See also [[https://hackers-1995.vercel.app/][similar looking web based demo]] !
 [[file:Screenshots/Mathematical formulas.png]]
 
 + TODO: instead of projecting 2D visualizations onto 3D space,
-  visualize some formula using all 3 dimensions avaliable.
+  visualize some formula using all 3 dimensions available.
 
 ** Sine heightmap and sphere
 :PROPERTIES:
@@ -191,7 +195,7 @@ Test scene that is generated simultaneously using:
 Instead of storing voxels in dumb [X * Y * Z] array, dynamically
 partitioned [[https://en.wikipedia.org/wiki/Octree][octree]] is used to compress data. Press "r" key anywhere in
 the scene to raytrace current view through compressed voxel
-datastructure.
+data structure.
 
 ** Graphics Benchmark
 :PROPERTIES:
@@ -202,22 +206,24 @@ datastructure.
 An automated graphics benchmark that measures the engine's rendering
 performance across different rendering modes.
 
-The benchmark creates a 16x16x16 grid of cubes (4096 total) and runs
-three tests sequentially, each for 30 seconds:
+[[file:Screenshots/Benchmark.png]]
 
-- *Solid Cubes* - Tests solid-color polygon rasterization
-- *Textured Cubes* - Tests textured polygon rendering with texture sampling
-- *Wireframe Cubes* - Tests line rendering performance
+The benchmark will cycle through different scenes that utilize different
+rendering primitives (textured polygons, billboards, solid polygons,
+etc.) to measure their relative performance.
 
 The camera follows a deterministic orbital path around the scene,
 ensuring reproducible results across runs.
 
-Example benchmark results:
+At the end, the benchmark will output a report that is easy to preserve for
+later comparisons.
+
+Example benchmark report:
 #+begin_example
 ================================================================================
                          GRAPHICS BENCHMARK RESULTS
 ================================================================================
-Date:        2026-03-15 20:16:01
+Date:        2026-03-16 19:17:51
 Resolution:  1920x1080
 Cubes:       4096 (16x16x16 grid)
 Duration:    30 seconds per test
@@ -227,28 +233,19 @@ SYSTEM INFORMATION
 --------------------------------------------------------------------------------
 CPU Name:    AMD Ryzen AI 9 HX 370 w/ Radeon 890M
 Arch:        amd64
-Cores:       24
+CPU cores:   24
 
 --------------------------------------------------------------------------------
 Test                         Avg FPS
 --------------------------------------------------------------------------------
-Solid Cubes                  35.65
-Textured Cubes               26.08
-Wireframe Cubes              33.67
-Star Grid                    256.88
+Solid Cubes                  49.65
+Lit Solid Cubes              41.40
+Textured Cubes               32.80
+Wireframe Cubes              42.84
+Star Grid                    304.59
 ================================================================================
 #+end_example
 
-** Developer tools
-:PROPERTIES:
-:CUSTOM_ID: developer-tools
-:ID:       8c5e2a1f-9d3b-4f6a-b8e7-1c4d5f7a9b2e
-:END:
-
-Press *F12* in any demo to open the Developer Tools panel. This
-debugging interface provides real-time insight into the rendering
-pipeline with diagnostic toggles.
-
 * Source code
 :PROPERTIES:
 :CUSTOM_ID: source-code
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/SineHeightmap.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/SineHeightmap.java
new file mode 100755 (executable)
index 0000000..91993f9
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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.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.basic.solidpolygon.SolidPolygon;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.WireframeSphere;
+
+/**
+ * Demo showing a sine heightmap surface with a central wireframe sphere.
+ * Two wobbly surfaces are positioned above and below the sphere.
+ */
+public class SineHeightmap {
+
+    /**
+     * Creates a new GraphDemo instance.
+     */
+    public SineHeightmap() {
+    }
+
+    /** Frequency of the wave pattern in the wobbly surfaces. */
+    private static final double WAVE_FREQUENCY = 50d;
+    /** Amplitude of the wave pattern in the wobbly surfaces. */
+    private static final double WAVE_AMPLITUDE = 50d;
+    /** Color for the square plates in the wobbly surfaces. */
+    private static final Color SQUARE_PLATE_COLOR = new Color("88F7");
+    /** Scale factor for the graph rendering. */
+    private static final double GRAPH_SCALE = 50d;
+
+    /**
+     * Creates a single square plate at the specified position.
+     * @param shapeCollection the collection to add the plate to
+     * @param y the Y coordinate (elevation)
+     * @param x the X coordinate
+     * @param z the Z coordinate
+     */
+    private static void makeSquarePlate(final ShapeCollection shapeCollection,
+                                        final double y, final double x, final double z) {
+        final Point3D p1 = new Point3D(x, y, z);
+        final Point3D p2 = new Point3D(x + 20, y, z);
+        final Point3D p3 = new Point3D(x, y, z + 20);
+        final Point3D p4 = new Point3D(x + 20, y, z + 20);
+        final SolidPolygon polygon1 = new SolidPolygon(p1, p2, p3, SQUARE_PLATE_COLOR);
+        final SolidPolygon polygon2 = new SolidPolygon(p4, p2, p3, SQUARE_PLATE_COLOR);
+        shapeCollection.addShape(polygon1);
+        shapeCollection.addShape(polygon2);
+    }
+
+    /**
+     * Creates a wobbly surface composed of square plates arranged in a wave pattern.
+     * @param shapeCollection the collection to add plates to
+     * @param surfaceElevation the base Y elevation of the surface
+     */
+    private static void addWobblySurface(final ShapeCollection shapeCollection,
+                                         final double surfaceElevation) {
+        for (double x = -500; x < 500; x += 20)
+            for (double z = -500; z < 500; z += 20) {
+
+                final double distanceFromCenter = Math.sqrt((x * x) + (z * z));
+
+                double plateElevation = Math.sin(distanceFromCenter / WAVE_FREQUENCY) * WAVE_AMPLITUDE;
+
+                makeSquarePlate(shapeCollection, plateElevation + surfaceElevation, x,
+                        z);
+            }
+    }
+
+    /**
+     * Entry point for the graph demo.
+     * @param args command line arguments (ignored)
+     */
+    public static void main(final String[] args) {
+
+        final ViewFrame viewFrame = new ViewFrame();
+        final ShapeCollection geometryCollection = viewFrame.getViewPanel()
+                .getRootShapeCollection();
+
+        addSphere(geometryCollection);
+        addWobblySurface(geometryCollection, 200);
+        addWobblySurface(geometryCollection, -200);
+
+        setCameraLocation(viewFrame);
+        
+        viewFrame.getViewPanel().ensureRenderThreadStarted();
+    }
+
+    /**
+     * Adds a wireframe sphere at the center of the scene.
+     * @param geometryCollection the collection to add the sphere to
+     */
+    private static void addSphere(ShapeCollection geometryCollection) {
+        geometryCollection.addShape(new WireframeSphere(new Point3D(0, 0, 0),
+                100,
+                new LineAppearance(
+                        4,
+                        new Color(255,0, 0, 30))
+        ));
+    }
+
+    /**
+     * Sets the camera to an initial viewing position.
+     * @param viewFrame the view frame whose camera to configure
+     */
+    private static void setCameraLocation(ViewFrame viewFrame) {
+        viewFrame.getViewPanel().getCamera().getTransform().setTranslation(new Point3D(0, 0, -500));
+    }
+
+}
index dec1514..14ab974 100644 (file)
@@ -5,6 +5,7 @@
 
 package eu.svjatoslav.sixth.e3d.examples.benchmark;
 
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
 import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
 
 /**
@@ -30,4 +31,12 @@ public interface BenchmarkTest {
      * @param shapes the shape collection to clean up
      */
     void teardown(ShapeCollection shapes);
+
+    /**
+     * Called after setup to provide the view panel for tests that need animation.
+     * Default implementation does nothing.
+     * @param viewPanel the view panel
+     */
+    default void setViewPanel(ViewPanel viewPanel) {
+    }
 }
\ No newline at end of file
index 96a50dc..bc0ccbb 100644 (file)
@@ -13,7 +13,9 @@ import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
 import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardInputHandler;
 import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
 
-import javax.swing.SwingUtilities;
+import javax.swing.*;
+import java.awt.*;
+import java.awt.datatransfer.StringSelection;
 import java.awt.event.KeyEvent;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -22,8 +24,8 @@ import java.util.List;
 
 /**
  * Automated graphics benchmark that tests the engine's rendering performance.
- * Runs multiple tests sequentially, each for a fixed duration, and outputs
- * reproducible benchmark results to standard output.
+ * Runs multiple tests sequentially, each for a fixed duration, and displays
+ * results in a dialog with copy-to-clipboard functionality.
  * 
  * <p>The benchmark creates a 16x16x16 grid of cubes (4096 total) with the camera
  * following a deterministic orbital path. Each test runs for 30 seconds by default.</p>
@@ -32,7 +34,8 @@ import java.util.List;
  * 
  * <p>Available tests:</p>
  * <ul>
- *   <li>{@link SolidCubesTest} - Solid-color polygon rendering</li>
+ *   <li>{@link SolidCubesTest} - Semi-transparent solid polygon rendering</li>
+ *   <li>{@link LitSolidCubesTest} - Opaque solid polygons with dynamic lighting</li>
  *   <li>{@link TexturedCubesTest} - Textured polygon rendering</li>
  *   <li>{@link WireframeCubesTest} - Line rendering</li>
  *   <li>{@link StarGridTest} - Billboard (glowing point) rendering</li>
@@ -68,15 +71,43 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
      * @param args command line arguments (ignored)
      */
     public static void main(String[] args) {
-        new GraphicsBenchmark();
+        SwingUtilities.invokeLater(() -> {
+            if (showIntroDialog()) {
+                new GraphicsBenchmark();
+            }
+        });
     }
 
-/**
+    private static boolean showIntroDialog() {
+        String message =
+                "<html><div style='width:400px; font-family: sans-serif;'>" +
+                "<h2>Graphics Benchmark</h2>" +
+                "<p>This will run a series of performance tests.</p>" +
+                "<ul>" +
+                "<li>Each test runs for 30 seconds</li>" +
+                "<li>Press <b>SPACE</b> to skip any test<br>" +
+                "<span style='color: gray;'>(skipping reduces measurement precision)</span></li>" +
+                "<li>A summary will be shown at the end</li>" +
+                "</ul>" +
+                "</div></html>";
+
+        int choice = JOptionPane.showConfirmDialog(
+                null,
+                message,
+                "Graphics Benchmark",
+                JOptionPane.OK_CANCEL_OPTION,
+                JOptionPane.INFORMATION_MESSAGE
+        );
+
+        return choice == JOptionPane.OK_OPTION;
+    }
+
+    /**
      * Constructs and runs the graphics benchmark.
      */
     public GraphicsBenchmark() {
-        registerTests();        // populate tests FIRST, before render thread starts
-        initializeWindow();     // now onFrame can safely access the tests list
+        registerTests();
+        initializeWindow();
     }
 
     private void initializeWindow() {
@@ -87,12 +118,12 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         viewPanel.addFrameListener(this);
         viewPanel.getKeyboardFocusStack().pushFocusOwner(this);
         camera = viewPanel.getCamera();
-        // Now explicitly start the render thread after all listeners are registered
         viewPanel.ensureRenderThreadStarted();
     }
 
     private void registerTests() {
         tests.add(new SolidCubesTest());
+        tests.add(new LitSolidCubesTest());
         tests.add(new TexturedCubesTest());
         tests.add(new WireframeCubesTest());
         tests.add(new StarGridTest());
@@ -112,6 +143,7 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         testStartTime = System.currentTimeMillis();
 
         currentTest.setup(shapes);
+        currentTest.setViewPanel(viewPanel);
     }
 
     private void finishCurrentTest() {
@@ -124,7 +156,6 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         double durationSeconds = elapsed / 1000.0;
         results.add(new TestResult(currentTest.getName(), frameCount, durationSeconds));
 
-        // Defer test transition to safe point (beginning of next frame)
         pendingTestTransition = true;
     }
 
@@ -146,39 +177,89 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
         viewPanel.removeFrameListener(this);
         viewPanel.stop();
         viewFrame.dispose();
-        printResults();
+
+        showResultsDialog();
     }
 
-    private void printResults() {
+    private String formatResults() {
+        StringBuilder sb = new StringBuilder();
         String separator = "================================================================================";
         String thinSeparator = "--------------------------------------------------------------------------------";
 
         Runtime runtime = Runtime.getRuntime();
 
-        System.out.println(separator);
-        System.out.println("                         GRAPHICS BENCHMARK RESULTS");
-        System.out.println(separator);
-        System.out.println("Date:        " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
-        System.out.println("Resolution:  " + WINDOW_WIDTH + "x" + WINDOW_HEIGHT);
-        System.out.println("Cubes:       " + (GRID_SIZE * GRID_SIZE * GRID_SIZE) + " (" + GRID_SIZE + "x" + GRID_SIZE + "x" + GRID_SIZE + " grid)");
-        System.out.println("Duration:    " + (TEST_DURATION_MS / 1000) + " seconds per test");
-        System.out.println();
-        System.out.println(thinSeparator);
-        System.out.println("SYSTEM INFORMATION");
-        System.out.println(thinSeparator);
-        System.out.println("CPU Name:    " + getCpuName());
-        System.out.println("Arch:        " + System.getProperty("os.arch"));
-        System.out.println("Cores:       " + runtime.availableProcessors());
-        System.out.println();
-        System.out.println(thinSeparator);
-        System.out.printf("%-28s %s%n", "Test", "Avg FPS");
-        System.out.println(thinSeparator);
+        sb.append(separator).append("\n");
+        sb.append("                         GRAPHICS BENCHMARK RESULTS\n");
+        sb.append(separator).append("\n");
+        sb.append("Date:        ").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("\n");
+        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("\n");
+        sb.append(thinSeparator).append("\n");
+        sb.append("SYSTEM INFORMATION\n");
+        sb.append(thinSeparator).append("\n");
+        sb.append("CPU Name:    ").append(getCpuName()).append("\n");
+        sb.append("Arch:        ").append(System.getProperty("os.arch")).append("\n");
+        sb.append("CPU cores:   ").append(runtime.availableProcessors()).append("\n");
+        sb.append("\n");
+        sb.append(thinSeparator).append("\n");
+        sb.append(String.format("%-28s %s%n", "Test", "Avg FPS"));
+        sb.append(thinSeparator).append("\n");
 
         for (TestResult result : results) {
-            System.out.printf("%-28s %.2f%n", result.testName, result.averageFps);
+            sb.append(String.format("%-28s %.2f%n", result.testName, result.averageFps));
         }
 
-        System.out.println(separator);
+        sb.append(separator).append("\n");
+
+        return sb.toString();
+    }
+
+    private void showResultsDialog() {
+        String resultsText = formatResults();
+
+        JTextArea textArea = new JTextArea(resultsText);
+        textArea.setEditable(false);
+        textArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
+        textArea.setCaretPosition(0);
+
+        JScrollPane scrollPane = new JScrollPane(textArea);
+        scrollPane.setPreferredSize(new Dimension(600, 400));
+
+        JButton copyButton = new JButton("Copy to Clipboard");
+        JButton closeButton = new JButton("Close");
+
+        copyButton.addActionListener(e -> {
+            StringSelection selection = new StringSelection(resultsText);
+            Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null);
+            copyButton.setText("Copied!");
+            copyButton.setEnabled(false);
+        });
+
+        closeButton.addActionListener(e -> {
+            Window window = SwingUtilities.getWindowAncestor(closeButton);
+            if (window != null) {
+                window.dispose();
+            }
+        });
+
+        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+        buttonPanel.add(copyButton);
+        buttonPanel.add(closeButton);
+
+        JPanel panel = new JPanel(new BorderLayout(0, 10));
+        panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+        panel.add(scrollPane, BorderLayout.CENTER);
+        panel.add(buttonPanel, BorderLayout.SOUTH);
+
+        JOptionPane.showMessageDialog(
+                null,
+                panel,
+                "Benchmark Results",
+                JOptionPane.PLAIN_MESSAGE
+        );
     }
 
     private String getCpuName() {
@@ -199,13 +280,11 @@ public class GraphicsBenchmark implements FrameListener, KeyboardInputHandler {
 
     @Override
     public boolean onFrame(ViewPanel viewPanel, int millisecondsSinceLastFrame) {
-        // Perform deferred test transition at safe point (before render cycle starts)
         if (pendingTestTransition) {
             pendingTestTransition = false;
             performTestTransition();
         }
 
-        // Deferred first-test start: runs on the render thread, before renderFrame()
         if (currentTest == null && !benchmarkFinished && results.isEmpty()) {
             startNextTest();
         }
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/LitSolidCubesTest.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/LitSolidCubesTest.java
new file mode 100644 (file)
index 0000000..0651938
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.benchmark;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.FrameListener;
+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.lighting.LightSource;
+import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.LightSourceMarker;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Benchmark test for solid opaque cubes with dynamic lighting.
+ * Renders a grid of fully opaque cubes lit by three orbiting light sources
+ * to test shaded polygon rasterization performance.
+ */
+public class LitSolidCubesTest implements BenchmarkTest {
+
+    private static final int GRID_SIZE = 16;
+    private static final double SPACING = 80;
+    private static final double CUBE_SIZE = 25;
+    private static final long RANDOM_SEED = 42;
+
+    private final Random random = new Random(RANDOM_SEED);
+    private final List<SolidPolygonCube> cubes = new ArrayList<>();
+    private final List<OrbitingLight> orbitingLights = new ArrayList<>();
+    private LightingManager lightingManager;
+    private FrameListener animator;
+    private ViewPanel viewPanel;
+
+    @Override
+    public String getName() {
+        return "Lit Solid Cubes";
+    }
+
+    @Override
+    public void setup(ShapeCollection shapes) {
+        random.setSeed(RANDOM_SEED);
+
+        lightingManager = new LightingManager();
+        lightingManager.setAmbientLight(new Color(15, 15, 20));
+
+        Color[] lightColors = {
+                new Color(255, 100, 100),
+                new Color(100, 255, 100),
+                new Color(100, 100, 255)
+        };
+
+        for (int i = 0; i < 3; i++) {
+            double angleOffset = i * (Math.PI * 2 / 3);
+            LightSource light = new LightSource(
+                    new Point3D(0, 0, 0),
+                    lightColors[i],
+                    2.5
+            );
+            lightingManager.addLight(light);
+
+            LightSourceMarker marker = new LightSourceMarker(light.getPosition(), lightColors[i]);
+            shapes.addShape(marker);
+
+            orbitingLights.add(new OrbitingLight(light, marker, 600, 0.002, angleOffset, i));
+        }
+
+        double offset = -(GRID_SIZE - 1) * SPACING / 2;
+
+        for (int x = 0; x < GRID_SIZE; x++) {
+            for (int y = 0; y < GRID_SIZE; y++) {
+                for (int z = 0; z < GRID_SIZE; z++) {
+                    double px = offset + x * SPACING;
+                    double py = offset + y * SPACING;
+                    double pz = offset + z * SPACING;
+
+                    Color color = new Color(
+                            150 + random.nextInt(105),
+                            150 + random.nextInt(105),
+                            150 + random.nextInt(105)
+                    );
+
+                    SolidPolygonCube cube = new SolidPolygonCube(
+                            new Point3D(px, py, pz),
+                            CUBE_SIZE,
+                            color
+                    );
+                    cube.setLightingManager(lightingManager);
+                    shapes.addShape(cube);
+                    cubes.add(cube);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void teardown(ShapeCollection shapes) {
+        for (SolidPolygonCube cube : cubes) {
+            shapes.getShapes().remove(cube);
+        }
+        cubes.clear();
+
+        for (OrbitingLight ol : orbitingLights) {
+            shapes.getShapes().remove(ol.marker);
+        }
+        orbitingLights.clear();
+
+        if (viewPanel != null && animator != null) {
+            viewPanel.removeFrameListener(animator);
+        }
+        viewPanel = null;
+        animator = null;
+        lightingManager = null;
+    }
+
+    /**
+     * Sets the view panel for animation callbacks.
+     * @param viewPanel the view panel
+     */
+    public void setViewPanel(ViewPanel viewPanel) {
+        this.viewPanel = viewPanel;
+        if (viewPanel != null) {
+            animator = new LightAnimator();
+            viewPanel.addFrameListener(animator);
+        }
+    }
+
+    private static class OrbitingLight {
+        final LightSource light;
+        final LightSourceMarker marker;
+        final double orbitRadius;
+        final double speed;
+        final int axisIndex;
+        double angle;
+
+        OrbitingLight(LightSource light, LightSourceMarker marker,
+                      double orbitRadius, double speed, double angleOffset, int axisIndex) {
+            this.light = light;
+            this.marker = marker;
+            this.orbitRadius = orbitRadius;
+            this.speed = speed;
+            this.angle = angleOffset;
+            this.axisIndex = axisIndex;
+        }
+    }
+
+    private class LightAnimator implements FrameListener {
+        @Override
+        public boolean onFrame(ViewPanel vp, int millisecondsSinceLastFrame) {
+            for (OrbitingLight ol : orbitingLights) {
+                ol.angle += ol.speed * millisecondsSinceLastFrame;
+
+                double x, y, z;
+                switch (ol.axisIndex) {
+                    case 0:
+                        x = ol.orbitRadius * Math.cos(ol.angle);
+                        y = ol.orbitRadius * 0.3 * Math.sin(ol.angle * 2);
+                        z = ol.orbitRadius * Math.sin(ol.angle);
+                        break;
+                    case 1:
+                        x = ol.orbitRadius * Math.sin(ol.angle);
+                        y = ol.orbitRadius * Math.cos(ol.angle);
+                        z = ol.orbitRadius * 0.3 * Math.sin(ol.angle * 2);
+                        break;
+                    default:
+                        x = ol.orbitRadius * 0.3 * Math.sin(ol.angle * 2);
+                        y = ol.orbitRadius * Math.sin(ol.angle);
+                        z = ol.orbitRadius * Math.cos(ol.angle);
+                        break;
+                }
+
+                Point3D newPos = new Point3D(x, y, z);
+                ol.light.setPosition(newPos);
+                ol.marker.setTransform(new eu.svjatoslav.sixth.e3d.math.Transform(newPos, 0, 0));
+            }
+            return true;
+        }
+    }
+}
\ No newline at end of file
index c89cfe6..32738e3 100644 (file)
@@ -103,17 +103,17 @@ public class TexturedCubesTest implements BenchmarkTest {
         Texture texture = new Texture(texSize, texSize, 2);
 
         java.awt.Graphics2D gr = texture.graphics;
-        gr.setBackground(new java.awt.Color(r, g, b, 80));
+        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) (255.0 * (glowWidth - i) / glowWidth);
+            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),
-                    200 - i * 30
+                    50 - i * 8
             );
             gr.setColor(glowColor);
             gr.drawRect(i, i, texSize - 1 - 2 * i, texSize - 1 - 2 * i);
index 7020f6d..afc26c4 100644 (file)
@@ -55,7 +55,8 @@ public class WireframeCubesTest implements BenchmarkTest {
                     Color color = new Color(
                             100 + random.nextInt(155),
                             100 + random.nextInt(155),
-                            100 + random.nextInt(155)
+                            100 + random.nextInt(155),
+                            50
                     );
                     LineAppearance appearance = new LineAppearance(5.5, color);
 
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/graph_demo/MathGraphsDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/graph_demo/MathGraphsDemo.java
new file mode 100644 (file)
index 0000000..b69ef47
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko. 
+ * This project is released under Creative Commons Zero (CC0) license.
+ *
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.graph_demo;
+
+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.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.Graph;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Demo showing mathematical function graphs rendered in 3D.
+ * Displays sine, cosine, tangent, and composite function graphs.
+ * Also includes a 3D surface graph showing z = (x² - y²) / 20 with
+ * intersecting planes and highlighted intersection curves.
+ */
+public class MathGraphsDemo {
+
+    private static final double GRAPH_SCALE = 50d;
+
+    /**
+     * Creates a new MathGraphsDemo instance.
+     */
+    public MathGraphsDemo() {
+    }
+
+    /**
+     * Creates a graph of the cosine function.
+     * @param location the position of the graph in 3D space
+     * @return a Graph component showing y = cos(x)
+     */
+    private static Graph getCosineGraph(final Point3D location) {
+        final List<Point2D> data = new ArrayList<>();
+        for (double x = 0; x < 20; x += 0.25) {
+            final double y = Math.cos(x);
+
+            final Point2D p = new Point2D(x, y);
+            data.add(p);
+        }
+
+        return new Graph(GRAPH_SCALE, data, "Cosine", location);
+    }
+
+    /**
+     * Creates a graph of y = sin(tan(x)).
+     * @param location the position of the graph in 3D space
+     * @return a Graph component showing the composite function
+     */
+    private static Graph getFormula1Graph(final Point3D location) {
+        final List<Point2D> data = new ArrayList<>();
+        for (double x = 0; x < 20; x += 0.25) {
+            final double y = Math.sin(Math.tan(x));
+
+            final Point2D p = new Point2D(x, y);
+            data.add(p);
+        }
+
+        return new Graph(GRAPH_SCALE, data, "y = sin(tan(x))", location);
+    }
+
+    /**
+     * Creates a graph of y = (10-x)^2 / 30.
+     * @param location the position of the graph in 3D space
+     * @return a Graph component showing the parabola
+     */
+    private static Graph getFormula2Graph(final Point3D location) {
+        final List<Point2D> data = new ArrayList<>();
+        for (double x = 0; x < 20; x += 0.25) {
+            final double y = (Math.pow((10 - x), 2) / 30) - 2;
+
+            final Point2D p = new Point2D(x, y);
+            data.add(p);
+        }
+
+        return new Graph(GRAPH_SCALE, data, "y = ( (10-x)^2 ) / 30", location);
+    }
+
+    /**
+     * Creates a graph of y = sin(x/2) + sin(x/1.26).
+     * @param location the position of the graph in 3D space
+     * @return a Graph component showing the composite sine wave
+     */
+    private static Graph getFormula3Graph(final Point3D location) {
+        final List<Point2D> data = new ArrayList<>();
+        for (double x = 0; x < 20; x += 0.25) {
+            final double y = Math.sin(x / 2) + Math.sin(x / 1.26);
+
+            final Point2D p = new Point2D(x, y);
+            data.add(p);
+        }
+
+        return new Graph(GRAPH_SCALE, data, "y = sin(x/2) + sin(x/1.26)", location);
+    }
+
+    /**
+     * Creates a graph of the sine function.
+     * @param location the position of the graph in 3D space
+     * @return a Graph component showing y = sin(x)
+     */
+    private static Graph getSineGraph(final Point3D location) {
+        final List<Point2D> data = new ArrayList<>();
+        for (double x = 0; x < 20; x += 0.25) {
+            final double y = Math.sin(x);
+
+            final Point2D p = new Point2D(x, y);
+            data.add(p);
+        }
+
+        return new Graph(GRAPH_SCALE, data, "Sine", location);
+    }
+
+    /**
+     * Creates a graph of the tangent function with clamped values.
+     * @param location the position of the graph in 3D space
+     * @return a Graph component showing y = tan(x) with clamped range
+     */
+    private static Graph getTangentGraph(final Point3D location) {
+        final List<Point2D> data = new ArrayList<>();
+        for (double x = 0; x < 20; x += 0.25) {
+            double y = Math.tan(x);
+
+            if (y > 2)
+                y = 2;
+            if (y < -2)
+                y = -2;
+
+            final Point2D p = new Point2D(x, y);
+            data.add(p);
+        }
+
+        return new Graph(GRAPH_SCALE, data, "Tangent", location);
+    }
+
+    /**
+     * Entry point for the math graphs demo.
+     * @param args command line arguments (ignored)
+     */
+    public static void main(final String[] args) {
+
+        final ViewFrame viewFrame = new ViewFrame();
+        final ShapeCollection geometryCollection = viewFrame.getViewPanel()
+                .getRootShapeCollection();
+
+        addMathFormulas(geometryCollection);
+        addSurfaceGraph(geometryCollection);
+
+        setCameraLocation(viewFrame);
+        
+        viewFrame.getViewPanel().ensureRenderThreadStarted();
+    }
+
+    /**
+     * Adds all mathematical formula graphs to the scene.
+     * @param geometryCollection the collection to add graphs to
+     */
+    private static void addMathFormulas(ShapeCollection geometryCollection) {
+        int z = 1000;
+        Point3D location = new Point3D(-600, -300, z);
+        geometryCollection.addShape(getSineGraph(location));
+
+        location = new Point3D(600, -300, z);
+        geometryCollection.addShape(getFormula1Graph(location));
+
+        location = new Point3D(-600, 0, z);
+        geometryCollection.addShape(getCosineGraph(location));
+
+        location = new Point3D(600, 0, z);
+        geometryCollection.addShape(getFormula2Graph(location));
+
+        location = new Point3D(-600, 300, z);
+        geometryCollection.addShape(getTangentGraph(location));
+
+        location = new Point3D(600, 300, z);
+        geometryCollection.addShape(getFormula3Graph(location));
+    }
+
+    private static void addSurfaceGraph(ShapeCollection geometryCollection) {
+        final double range = 10;
+        final double step = 0.5;
+        final double scale = 30;
+
+        final SurfaceGraph3D.MathFunction3D function = (x, y) -> (x * x - y * y) / 20;
+
+        SurfaceGraph3D surface = new SurfaceGraph3D(
+                -range, range, step,
+                -range, range, step,
+                function,
+                new Color(150, 150, 150, 180),
+                Color.WHITE,
+                scale,
+                new Point3D(0, 0, 0)
+        );
+
+        surface.addYIntersectionCurve(4, function, new Color(255, 255, 0, 220), 1.2);
+        surface.addXIntersectionCurve(-4, function, new Color(255, 120, 0, 220), 1.2);
+
+        geometryCollection.addShape(surface);
+    }
+
+    /**
+     * Sets the camera to an initial viewing position.
+     * @param viewFrame the view frame whose camera to configure
+     */
+    private static void setCameraLocation(ViewFrame viewFrame) {
+        viewFrame.getViewPanel().getCamera().getTransform().setTranslation(new Point3D(0, 0, -500));
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/graph_demo/SurfaceGraph3D.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/graph_demo/SurfaceGraph3D.java
new file mode 100644 (file)
index 0000000..661a854
--- /dev/null
@@ -0,0 +1,197 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.examples.graph_demo;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A 3D surface graph that visualizes mathematical functions of the form z = f(x, y).
+ *
+ * <p>The surface is rendered as a grid of quadrilaterals (split into triangles) with
+ * optional wireframe overlay. The surface color can be customized, and the wireframe
+ * lines are drawn in a contrasting color.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create a saddle surface: z = x^2 - y^2
+ * SurfaceGraph3D saddle = new SurfaceGraph3D(
+ *     -5, 5, 0.5,      // x range and step
+ *     -5, 5, 0.5,      // y range and step
+ *     (x, y) -> x * x - y * y,  // function z = f(x, y)
+ *     new Color(180, 180, 180, 200),  // gray semi-transparent surface
+ *     new Color(255, 255, 255),       // white grid lines
+ *     new Point3D(0, 0, 0)            // position in 3D space
+ * );
+ * shapeCollection.addShape(saddle);
+ * }</pre>
+ *
+ * @see AbstractCompositeShape
+ * @see SolidPolygon
+ * @see Line
+ */
+public class SurfaceGraph3D extends AbstractCompositeShape {
+
+    private final double xMin;
+    private final double xMax;
+    private final double xStep;
+    private final double yMin;
+    private final double yMax;
+    private final double yStep;
+    private final double scale;
+    private final Color surfaceColor;
+    private final Color gridColor;
+    private final double lineWidth;
+
+    /**
+     * Creates a 3D surface graph at the specified location.
+     *
+     * @param xMin         minimum X value of the domain
+     * @param xMax         maximum X value of the domain
+     * @param xStep        step size between grid points along X axis
+     * @param yMin         minimum Y value of the domain
+     * @param yMax         maximum Y value of the domain
+     * @param yStep        step size between grid points along Y axis
+     * @param function     the mathematical function z = f(x, y) to visualize
+     * @param surfaceColor color of the surface polygons (use semi-transparent for see-through effect)
+     * @param gridColor    color of the wireframe grid lines
+     * @param lineWidth    width of grid lines in world units
+     * @param scale        scale factor applied to all generated vertices
+     * @param location     the 3D position of the graph's origin
+     */
+    public SurfaceGraph3D(final double xMin, final double xMax, final double xStep,
+                          final double yMin, final double yMax, final double yStep,
+                          final MathFunction3D function,
+                          final Color surfaceColor, final Color gridColor,
+                          final double lineWidth, final double scale, final Point3D location) {
+        super(location);
+
+        this.xMin = xMin;
+        this.xMax = xMax;
+        this.yMin = yMin;
+        this.yMax = yMax;
+        this.xStep = xStep;
+        this.yStep = yStep;
+        this.scale = scale;
+        this.surfaceColor = surfaceColor;
+        this.gridColor = gridColor;
+        this.lineWidth = lineWidth;
+
+        generateSurface(function);
+    }
+
+    public SurfaceGraph3D(final double xMin, final double xMax, final double xStep,
+                          final double yMin, final double yMax, final double yStep,
+                          final MathFunction3D function,
+                          final Color surfaceColor, final Color gridColor,
+                          final double scale, final Point3D location) {
+        this(xMin, xMax, xStep, yMin, yMax, yStep, function, surfaceColor, gridColor, 0.1, scale, location);
+    }
+
+    private void generateSurface(final MathFunction3D function) {
+        final int xCount = (int) ((xMax - xMin) / xStep) + 1;
+        final int yCount = (int) ((yMax - yMin) / yStep) + 1;
+
+        final Point3D[][] vertices = new Point3D[xCount][yCount];
+
+        for (int i = 0; i < xCount; i++) {
+            for (int j = 0; j < yCount; j++) {
+                final double x = xMin + i * xStep;
+                final double y = yMin + j * yStep;
+                final double z = function.apply(x, y);
+                vertices[i][j] = new Point3D(x * scale, -z * scale, y * scale);
+            }
+        }
+
+        for (int i = 0; i < xCount - 1; i++) {
+            for (int j = 0; j < yCount - 1; j++) {
+                final Point3D p1 = vertices[i][j];
+                final Point3D p2 = vertices[i + 1][j];
+                final Point3D p3 = vertices[i][j + 1];
+                final Point3D p4 = vertices[i + 1][j + 1];
+
+                addQuad(p1, p2, p3, p4);
+                addGridLines(p1, p2, p3, p4);
+            }
+        }
+    }
+
+    private void addQuad(final Point3D p1, final Point3D p2, final Point3D p3, final Point3D p4) {
+        final SolidPolygon poly1 = new SolidPolygon(p1, p2, p3, surfaceColor);
+        poly1.setBackfaceCulling(false);
+        addShape(poly1);
+
+        final SolidPolygon poly2 = new SolidPolygon(p2, p4, p3, surfaceColor);
+        poly2.setBackfaceCulling(false);
+        addShape(poly2);
+    }
+
+    private void addGridLines(final Point3D p1, final Point3D p2, final Point3D p3, final Point3D p4) {
+        addShape(new Line(p1, p2, gridColor, lineWidth));
+        addShape(new Line(p2, p4, gridColor, lineWidth));
+        addShape(new Line(p3, p4, gridColor, lineWidth));
+        addShape(new Line(p1, p3, gridColor, lineWidth));
+    }
+
+    /**
+     * Adds a curve highlighting the intersection of the surface with a vertical plane at constant X.
+     *
+     * @param xValue   the X coordinate of the intersecting plane
+     * @param function the mathematical function z = f(x, y)
+     * @param color    the color of the intersection curve
+     * @param width    the line width
+     */
+    public void addXIntersectionCurve(final double xValue, final MathFunction3D function,
+                                      final Color color, final double width) {
+        Point3D prevPoint = null;
+        for (double y = yMin; y <= yMax; y += 0.15) {
+            final double z = function.apply(xValue, y);
+            final Point3D currentPoint = new Point3D(xValue * scale, -z * scale, y * scale);
+            if (prevPoint != null) {
+                addShape(new Line(prevPoint, currentPoint, color, width));
+            }
+            prevPoint = currentPoint;
+        }
+    }
+
+    /**
+     * Adds a curve highlighting the intersection of the surface with a vertical plane at constant Y.
+     *
+     * @param yValue   the Y coordinate of the intersecting plane
+     * @param function the mathematical function z = f(x, y)
+     * @param color    the color of the intersection curve
+     * @param width    the line width
+     */
+    public void addYIntersectionCurve(final double yValue, final MathFunction3D function,
+                                      final Color color, final double width) {
+        Point3D prevPoint = null;
+        for (double x = xMin; x <= xMax; x += 0.15) {
+            final double z = function.apply(x, yValue);
+            final Point3D currentPoint = new Point3D(x * scale, -z * scale, yValue * scale);
+            if (prevPoint != null) {
+                addShape(new Line(prevPoint, currentPoint, color, width));
+            }
+            prevPoint = currentPoint;
+        }
+    }
+
+    /**
+     * Functional interface for 3D mathematical functions of the form z = f(x, y).
+     */
+    @FunctionalInterface
+    public interface MathFunction3D {
+        /**
+         * Computes the Z value for given X and Y coordinates.
+         *
+         * @param x the X coordinate
+         * @param y the Y coordinate
+         * @return the Z value (height) at the given (x, y) position
+         */
+        double apply(double x, double y);
+    }
+}
index bd0d9a8..7aa936d 100644 (file)
@@ -6,7 +6,8 @@
 
 package eu.svjatoslav.sixth.e3d.examples.launcher;
 
-import eu.svjatoslav.sixth.e3d.examples.GraphDemo;
+import eu.svjatoslav.sixth.e3d.examples.SineHeightmap;
+import eu.svjatoslav.sixth.e3d.examples.graph_demo.MathGraphsDemo;
 import eu.svjatoslav.sixth.e3d.examples.MinimalExample;
 import eu.svjatoslav.sixth.e3d.examples.OctreeDemo;
 import eu.svjatoslav.sixth.e3d.examples.RandomPolygonsDemo;
@@ -39,8 +40,11 @@ class ApplicationListPanel extends JPanel {
         new DemoEntry("Volumetric Octree",
                 "Octree-based rendering with on-demand raytracing",
                 new ShowOctree()),
-        new DemoEntry("Mathematical graphs",
-                "Function graphs rendered in 3D around a sphere",
+        new DemoEntry("Sine heightmap",
+                "Two wobbly sine wave surfaces with central sphere",
+                new ShowSineHeightmap()),
+        new DemoEntry("Math graphs demo",
+                "Function graphs (sin, cos, tan) rendered in 3D",
                 new ShowMathGraphs()),
         new DemoEntry("Point cloud galaxy",
                 "Spiral galaxy with 10,000 glowing points",
@@ -169,7 +173,18 @@ class ApplicationListPanel extends JPanel {
 
         @Override
         public void actionPerformed(final ActionEvent e) {
-            GraphDemo.main(null);
+            MathGraphsDemo.main(null);
+        }
+    }
+
+    private static class ShowSineHeightmap extends AbstractAction {
+        ShowSineHeightmap() {
+            putValue(NAME, "Sine heightmap");
+        }
+
+        @Override
+        public void actionPerformed(final ActionEvent e) {
+            SineHeightmap.main(null);
         }
     }