feat(graph): add SurfaceGraph3D class for 3D surface visualization
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Wed, 18 Mar 2026 19:49:20 +0000 (21:49 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Wed, 18 Mar 2026 19:49:20 +0000 (21:49 +0200)
Add new SurfaceGraph3D composite shape that renders mathematical functions
z = f(x,y) as colored surface grids with wireframe overlay. Includes
intersection curve visualization for planes at constant X or Y values.

Also reorganize MathGraphsDemo into graph_demo package and integrate
the new surface graph demonstrating a saddle function with intersection
curves.

src/main/java/eu/svjatoslav/sixth/e3d/examples/MathGraphsDemo.java [deleted file]
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/src/main/java/eu/svjatoslav/sixth/e3d/examples/MathGraphsDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/MathGraphsDemo.java
deleted file mode 100644 (file)
index 49bdd88..0000000
+++ /dev/null
@@ -1,189 +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.Point2D;
-import eu.svjatoslav.sixth.e3d.geometry.Point3D;
-import eu.svjatoslav.sixth.e3d.gui.ViewFrame;
-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.
- */
-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);
-
-        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));
-    }
-
-    /**
-     * 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/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 272261d..7aa936d 100644 (file)
@@ -7,7 +7,7 @@
 package eu.svjatoslav.sixth.e3d.examples.launcher;
 
 import eu.svjatoslav.sixth.e3d.examples.SineHeightmap;
-import eu.svjatoslav.sixth.e3d.examples.MathGraphsDemo;
+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;