feat(demo): add diamond-square procedural terrain demo
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 22 Mar 2026 19:16:32 +0000 (21:16 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 22 Mar 2026 19:16:32 +0000 (21:16 +0200)
Add DiamondSquareLandscape demo showcasing procedural terrain
generation with 3 colored light sources.

Update all demos to use global LightingManager from ViewPanel instead
of creating per-demo instances. Update Quaternion API calls from
setQuaternion() to set().

Squashed commits:
- feat(demo): add diamond-square procedural terrain demo
- refactor(demos): update demos to use global LightingManager
- refactor: update demos to use Quaternion.set() instead of setQuaternion()

AGENTS.md
src/main/java/eu/svjatoslav/sixth/e3d/examples/ShadedShapesDemo.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/TextEditorDemo2.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/benchmark/LitSolidCubesTest.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/diamondsquare_demo/DiamondSquareLandscape.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/diamondsquare_demo/DiamondSquareTerrain.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/launcher/ApplicationListPanel.java
src/main/java/eu/svjatoslav/sixth/e3d/examples/life_demo/Main.java

index 899d46f..872d3c3 100644 (file)
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -61,6 +61,9 @@ src/main/java/eu/svjatoslav/sixth/e3d/examples/
 │   ├── Cell.java
 │   ├── Matrix.java
 │   └── Star.java
+├── diamondsquare_demo/   - Procedural terrain generation demo
+│   ├── DiamondSquareLandscape.java
+│   └── DiamondSquareTerrain.java
 ├── galaxy_demo/        - Galaxy simulation demo
 │   ├── Galaxy.java
 │   └── PointCloudDemo.java
index 49f121a..f8232c5 100644 (file)
@@ -6,7 +6,6 @@ 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 eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCylinder;
@@ -36,36 +35,36 @@ public class ShadedShapesDemo {
      * Entry point for the shaded shapes demo.
      * @param args command line arguments (ignored)
      */
-    public static void main(String[] args) {
-        ViewFrame viewFrame = new ViewFrame();
-        ViewPanel viewPanel = viewFrame.getViewPanel();
-        ShapeCollection shapes = viewPanel.getRootShapeCollection();
+    public static void main(final String[] args) {
+        final ViewFrame viewFrame = new ViewFrame();
+        final ViewPanel viewPanel = viewFrame.getViewPanel();
+        final ShapeCollection shapes = viewPanel.getRootShapeCollection();
 
-        LightingManager lightingManager = new LightingManager();
-        lightingManager.setAmbientLight(new Color(25, 25, 25));
+        // Use the global lighting manager from ViewPanel
+        viewPanel.getLightingManager().setAmbientLight(new Color(25, 25, 25));
 
-        Random random = new Random(42);
-        List<OrbitingLight> orbitingLights = new ArrayList<>();
+        final Random random = new Random(42);
+        final List<OrbitingLight> orbitingLights = new ArrayList<>();
 
         for (int i = 0; i < LIGHT_COUNT; i++) {
-            Color color = new Color(
+            final Color color = new Color(
                     random.nextInt(256),
                     random.nextInt(256),
                     random.nextInt(256)
             );
 
-            double orbitRadius = 500 + random.nextInt(200);
-            double speed = 0.01 + random.nextDouble() * 0.03;
-            double angleOffset = random.nextDouble() * Math.PI * 2;
-            double intensity = 2.0 + random.nextDouble() * 3.0;
+            final double orbitRadius = 500 + random.nextInt(200);
+            final double speed = 0.01 + random.nextDouble() * 0.03;
+            final double angleOffset = random.nextDouble() * Math.PI * 2;
+            final double intensity = 2.0 + random.nextDouble() * 3.0;
 
-            int axis = random.nextInt(3);
-            double ellipseFactor = 0.7 + random.nextDouble() * 0.3;
+            final int axis = random.nextInt(3);
+            final double ellipseFactor = 0.7 + random.nextDouble() * 0.3;
 
-            LightSource light = new LightSource(new Point3D(0, 0, 0), color, intensity);
-            LightSourceMarker marker = new LightSourceMarker(light.getPosition(), color);
+            final LightSource light = new LightSource(new Point3D(0, 0, 0), color, intensity);
+            final LightSourceMarker marker = new LightSourceMarker(light.getPosition(), color);
 
-            lightingManager.addLight(light);
+            viewPanel.getLightingManager().addLight(light);
             shapes.addShape(marker);
 
             orbitingLights.add(new OrbitingLight(
@@ -75,37 +74,37 @@ public class ShadedShapesDemo {
         }
 
         // Sphere
-        SolidPolygonSphere sphere = new SolidPolygonSphere(
+        final SolidPolygonSphere sphere = new SolidPolygonSphere(
                 new Point3D(-400, 0, 0), 150, 28, new Color(200, 210, 255)
         );
-        sphere.setLightingManager(lightingManager);
+        sphere.setShadingEnabled(true);
         shapes.addShape(sphere);
 
         // Pyramid
-        SolidPolygonPyramid pyramid = new SolidPolygonPyramid(
+        final SolidPolygonPyramid pyramid = new SolidPolygonPyramid(
                 new Point3D(0, 130, 0), 150, 260, new Color(255, 200, 200)
         );
-        pyramid.setLightingManager(lightingManager);
+        pyramid.setShadingEnabled(true);
         shapes.addShape(pyramid);
 
         // Cube
-        SolidPolygonCube cube = new SolidPolygonCube(
+        final SolidPolygonCube cube = new SolidPolygonCube(
                 new Point3D(400, 0, 0), 150, new Color(200, 255, 200)
         );
-        cube.setLightingManager(lightingManager);
+        cube.setShadingEnabled(true);
         shapes.addShape(cube);
 
         // Cylinder
-        SolidPolygonCylinder cylinder = new SolidPolygonCylinder(
+        final SolidPolygonCylinder cylinder = new SolidPolygonCylinder(
                 new Point3D(800, 0, 0), 120, 260, 24, new Color(255, 220, 180)
         );
-        cylinder.setLightingManager(lightingManager);
+        cylinder.setShadingEnabled(true);
         shapes.addShape(cylinder);
 
         // Camera
         viewPanel.getCamera().getTransform().setTranslation(new Point3D(200, -250, -900));
 
-        MultiLightAnimator animator = new MultiLightAnimator(orbitingLights);
+        final MultiLightAnimator animator = new MultiLightAnimator(orbitingLights);
         viewPanel.addFrameListener(animator);
 
         viewPanel.repaintDuringNextViewUpdate();
index 49640dd..773fe43 100644 (file)
@@ -96,6 +96,6 @@ public class TextEditorDemo {
     private static void setCameraLocation(ViewPanel viewPanel) {
         Camera camera = viewPanel.getCamera();
         camera.getTransform().setTranslation(new Point3D(500, -300, -800));
-        camera.getTransform().getRotation().setQuaternion(Quaternion.fromAngles(0.6, -0.5));
+        camera.getTransform().getRotation().set(Quaternion.fromAngles(0.6, -0.5));
     }
 }
index 0539fb4..75a4413 100644 (file)
@@ -194,6 +194,6 @@ public class TextEditorDemo2 {
     private static void setCameraLocation(ViewPanel viewPanel) {
         Camera camera = viewPanel.getCamera();
         camera.getTransform().setTranslation(new Point3D(500, -300, -800));
-        camera.getTransform().getRotation().setQuaternion(Quaternion.fromAngles(0.6, -0.5));
+        camera.getTransform().getRotation().set(Quaternion.fromAngles(0.6, -0.5));
     }
 }
index c45246c..9fd491e 100644 (file)
@@ -19,6 +19,8 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Random;
 
+import static eu.svjatoslav.sixth.e3d.math.Transform.*;
+
 /**
  * Benchmark test for solid opaque cubes with dynamic lighting.
  * Renders a grid of fully opaque cubes lit by three orbiting light sources
@@ -34,7 +36,6 @@ public class LitSolidCubesTest implements BenchmarkTest {
     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;
 
@@ -44,54 +45,51 @@ public class LitSolidCubesTest implements BenchmarkTest {
     }
 
     @Override
-    public void setup(ShapeCollection shapes) {
+    public void setup(final ShapeCollection shapes) {
         random.setSeed(RANDOM_SEED);
 
-        lightingManager = new LightingManager();
-        lightingManager.setAmbientLight(new Color(15, 15, 20));
-
-        Color[] lightColors = {
+        // Note: lights will be added when setViewPanel is called
+        final 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(
+            final double angleOffset = i * (Math.PI * 2 / 3);
+            final LightSource light = new LightSource(
                     new Point3D(0, 0, 0),
                     lightColors[i],
                     2.5
             );
-            lightingManager.addLight(light);
 
-            LightSourceMarker marker = new LightSourceMarker(light.getPosition(), lightColors[i]);
+            final 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;
+        final 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;
+                    final double px = offset + x * SPACING;
+                    final double py = offset + y * SPACING;
+                    final double pz = offset + z * SPACING;
 
-                    Color color = new Color(
+                    final Color color = new Color(
                             150 + random.nextInt(105),
                             150 + random.nextInt(105),
                             150 + random.nextInt(105)
                     );
 
-                    SolidPolygonCube cube = new SolidPolygonCube(
+                    final SolidPolygonCube cube = new SolidPolygonCube(
                             new Point3D(px, py, pz),
                             CUBE_SIZE,
                             color
                     );
-                    cube.setLightingManager(lightingManager);
+                    cube.setShadingEnabled(true);
                     shapes.addShape(cube);
                     cubes.add(cube);
                 }
@@ -100,14 +98,18 @@ public class LitSolidCubesTest implements BenchmarkTest {
     }
 
     @Override
-    public void teardown(ShapeCollection shapes) {
-        for (SolidPolygonCube cube : cubes) {
+    public void teardown(final ShapeCollection shapes) {
+        for (final SolidPolygonCube cube : cubes) {
             shapes.getShapes().remove(cube);
         }
         cubes.clear();
 
-        for (OrbitingLight ol : orbitingLights) {
+        for (final OrbitingLight ol : orbitingLights) {
             shapes.getShapes().remove(ol.marker);
+            // Remove light from global lighting manager
+            if (viewPanel != null) {
+                viewPanel.getLightingManager().removeLight(ol.light);
+            }
         }
         orbitingLights.clear();
 
@@ -116,16 +118,22 @@ public class LitSolidCubesTest implements BenchmarkTest {
         }
         viewPanel = null;
         animator = null;
-        lightingManager = null;
     }
 
     /**
      * Sets the view panel for animation callbacks.
      * @param viewPanel the view panel
      */
-    public void setViewPanel(ViewPanel viewPanel) {
+    public void setViewPanel(final ViewPanel viewPanel) {
         this.viewPanel = viewPanel;
         if (viewPanel != null) {
+            // Configure global lighting
+            LightingManager lightingManager = viewPanel.getLightingManager();
+            lightingManager.setAmbientLight(new Color(15, 15, 20));
+            for (final OrbitingLight ol : orbitingLights) {
+                lightingManager.addLight(ol.light);
+            }
+
             animator = new LightAnimator();
             viewPanel.addFrameListener(animator);
         }
@@ -139,8 +147,8 @@ public class LitSolidCubesTest implements BenchmarkTest {
         final int axisIndex;
         double angle;
 
-        OrbitingLight(LightSource light, LightSourceMarker marker,
-                      double orbitRadius, double speed, double angleOffset, int axisIndex) {
+        OrbitingLight(final LightSource light, final LightSourceMarker marker,
+                      final double orbitRadius, final double speed, final double angleOffset, final int axisIndex) {
             this.light = light;
             this.marker = marker;
             this.orbitRadius = orbitRadius;
@@ -152,8 +160,8 @@ public class LitSolidCubesTest implements BenchmarkTest {
 
     private class LightAnimator implements FrameListener {
         @Override
-        public boolean onFrame(ViewPanel vp, int millisecondsSinceLastFrame) {
-            for (OrbitingLight ol : orbitingLights) {
+        public boolean onFrame(final ViewPanel vp, final int millisecondsSinceLastFrame) {
+            for (final OrbitingLight ol : orbitingLights) {
                 ol.angle += ol.speed * millisecondsSinceLastFrame;
 
                 double x, y, z;
@@ -175,9 +183,9 @@ public class LitSolidCubesTest implements BenchmarkTest {
                         break;
                 }
 
-                Point3D newPos = new Point3D(x, y, z);
+                final Point3D newPos = new Point3D(x, y, z);
                 ol.light.setPosition(newPos);
-                ol.marker.setTransform(eu.svjatoslav.sixth.e3d.math.Transform.fromAngles(newPos, 0, 0));
+                ol.marker.setTransform(fromAngles(newPos, 0, 0));
             }
             return true;
         }
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/diamondsquare_demo/DiamondSquareLandscape.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/diamondsquare_demo/DiamondSquareLandscape.java
new file mode 100644 (file)
index 0000000..2328e4d
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.diamondsquare_demo;
+
+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.lighting.LightSource;
+import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.LightSourceMarker;
+
+/**
+ * Demo showing a procedurally generated mountain landscape using the diamond-square algorithm.
+ * Three colored light sources (warm orange, cool cyan, neutral white) illuminate the terrain
+ * from above, demonstrating dynamic flat shading.
+ */
+public class DiamondSquareLandscape {
+
+    /**
+     * Entry point for the diamond-square landscape demo.
+     * @param args command line arguments (ignored)
+     */
+    public static void main(final String[] args) {
+        final ViewFrame viewFrame = new ViewFrame();
+        ViewPanel viewPanel = viewFrame.getViewPanel();
+
+        final ShapeCollection shapes = viewPanel.getRootShapeCollection();
+        
+        setLights(viewFrame, shapes);
+
+        shapes.addShape(new DiamondSquareTerrain(129, 0.0, 300.0, 42));
+
+        viewPanel.getCamera().getTransform()
+                .setTranslation(new Point3D(0, -600, -800));
+
+        viewPanel.ensureRenderThreadStarted();
+    }
+
+    // TODO: add javadoc here
+    private static void setLights(ViewFrame viewFrame, ShapeCollection shapes) {
+        LightingManager lightingManager = viewFrame.getViewPanel().getLightingManager();
+        lightingManager.setAmbientLight(new Color(250, 150, 100));
+
+        final LightSource warmLight = new LightSource(
+                new Point3D(-400, -500, 0),
+                new Color(255, 180, 100),
+                190.0
+        );
+        final LightSource coolLight = new LightSource(
+                new Point3D(400, -500, 0),
+                new Color(100, 200, 255),
+                220.0
+        );
+        final LightSource neutralLight = new LightSource(
+                new Point3D(0, -600, 300),
+                new Color(255, 255, 255),
+                250.0
+        );
+
+        lightingManager.addLight(warmLight);
+        lightingManager.addLight(coolLight);
+        lightingManager.addLight(neutralLight);
+
+        shapes.addShape(new LightSourceMarker(warmLight.getPosition(), new Color(255, 180, 100)));
+        shapes.addShape(new LightSourceMarker(coolLight.getPosition(), new Color(100, 200, 255)));
+        shapes.addShape(new LightSourceMarker(neutralLight.getPosition(), new Color(255, 255, 255)));
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/diamondsquare_demo/DiamondSquareTerrain.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/diamondsquare_demo/DiamondSquareTerrain.java
new file mode 100644 (file)
index 0000000..32bd138
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.diamondsquare_demo;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.math.DiamondSquare;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A procedurally generated terrain mesh using the diamond-square algorithm.
+ * Creates a grid of triangular polygons with height variation for mountains.
+ */
+public class DiamondSquareTerrain extends AbstractCompositeShape {
+
+    private static final double TERRAIN_SIZE = 800.0;
+
+    /**
+     * Creates a diamond-square terrain with the specified parameters.
+     *
+     * @param gridSize  the size of the heightmap grid (must be 2^n + 1)
+     * @param minHeight the minimum terrain height
+     * @param maxHeight the maximum terrain height
+     * @param seed      random seed for reproducible terrain
+     */
+    public DiamondSquareTerrain(final int gridSize, final double minHeight, final double maxHeight, final long seed) {
+        super();
+
+        final double[][] heightmap = DiamondSquare.generateMap(gridSize, minHeight, maxHeight, seed);
+        createPolygons(gridSize, heightmap);
+        setBackfaceCulling(true);
+    }
+
+    /**
+     * Creates triangular polygons from the heightmap.
+     */
+    private void createPolygons(final int gridSize, final double[][] heightmap) {
+        final double cellSize = TERRAIN_SIZE / (gridSize - 1);
+        final double offsetX = -TERRAIN_SIZE / 2;
+        final double offsetZ = -TERRAIN_SIZE / 2;
+
+        for (int z = 0; z < gridSize - 1; z++) {
+            for (int x = 0; x < gridSize - 1; x++) {
+                final double x0 = offsetX + x * cellSize;
+                final double x1 = offsetX + (x + 1) * cellSize;
+                final double z0 = offsetZ + z * cellSize;
+                final double z1 = offsetZ + (z + 1) * cellSize;
+
+                final double y00 = heightmap[z][x];
+                final double y10 = heightmap[z][x + 1];
+                final double y01 = heightmap[z + 1][x];
+                final double y11 = heightmap[z + 1][x + 1];
+
+                final Color color = new Color(10, 10, 10);
+
+                final Point3D p00 = new Point3D(x0, y00, z0);
+                final Point3D p10 = new Point3D(x1, y10, z0);
+                final Point3D p01 = new Point3D(x0, y01, z1);
+                final Point3D p11 = new Point3D(x1, y11, z1);
+
+                final SolidPolygon tri1 = new SolidPolygon(p00, p10, p01, color);
+                final SolidPolygon tri2 = new SolidPolygon(p10, p11, p01, color);
+
+                tri1.setShadingEnabled(true);
+                tri2.setShadingEnabled(true);
+
+                addShape(tri1);
+                addShape(tri2);
+            }
+        }
+    }
+}
\ No newline at end of file
index 7aa936d..7e978aa 100644 (file)
@@ -6,6 +6,7 @@
 
 package eu.svjatoslav.sixth.e3d.examples.launcher;
 
+import eu.svjatoslav.sixth.e3d.examples.diamondsquare_demo.DiamondSquareLandscape;
 import eu.svjatoslav.sixth.e3d.examples.SineHeightmap;
 import eu.svjatoslav.sixth.e3d.examples.graph_demo.MathGraphsDemo;
 import eu.svjatoslav.sixth.e3d.examples.MinimalExample;
@@ -67,6 +68,9 @@ class ApplicationListPanel extends JPanel {
         new DemoEntry("Shaded Shapes",
                 "Shapes lit by orbiting colored light sources",
                 new ShowShadedShapes()),
+        new DemoEntry("Diamond-Square Landscape",
+                "Procedural mountains with 3 colored light sources",
+                new ShowDiamondSquareLandscape()),
         new DemoEntry("Graphics Benchmark",
                 "Automated performance measuring FPS across modes",
                 new ShowGraphicsBenchmark()),
@@ -232,6 +236,17 @@ class ApplicationListPanel extends JPanel {
         }
     }
 
+    private static class ShowDiamondSquareLandscape extends AbstractAction {
+        ShowDiamondSquareLandscape() {
+            putValue(NAME, "Diamond-Square Landscape");
+        }
+
+        @Override
+        public void actionPerformed(final ActionEvent e) {
+            DiamondSquareLandscape.main(null);
+        }
+    }
+
     private static class ShowGraphicsBenchmark extends AbstractAction {
         ShowGraphicsBenchmark() {
             putValue(NAME, "Graphics Benchmark");
index 0c1b4b6..eaa2d28 100644 (file)
@@ -131,7 +131,7 @@ public class Main extends WorldNavigationUserInputTracker {
      */
     private void setCameraOrientation(final Camera camera) {
         camera.getTransform().setTranslation(new Point3D(100, -50, -200));
-        camera.getTransform().getRotation().setQuaternion(Quaternion.fromAngles(0.2f, -0.7f));
+        camera.getTransform().getRotation().set(Quaternion.fromAngles(0.2f, -0.7f));
     }
 
 }