#+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
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
: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
|--------------------------------+--------------------------------------|
| 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
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
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
[[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:
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:
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
--------------------------------------------------------------------------------
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
--- /dev/null
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ *
+ */
+
+package eu.svjatoslav.sixth.e3d.examples;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.ViewFrame;
+import eu.svjatoslav.sixth.e3d.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));
+ }
+
+}
package eu.svjatoslav.sixth.e3d.examples.benchmark;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
/**
* @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
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;
/**
* 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>
*
* <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>
* @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() {
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());
testStartTime = System.currentTimeMillis();
currentTest.setup(shapes);
+ currentTest.setViewPanel(viewPanel);
}
private void finishCurrentTest() {
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;
}
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() {
@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();
}
--- /dev/null
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.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
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);
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);
--- /dev/null
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ *
+ */
+
+package eu.svjatoslav.sixth.e3d.examples.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
--- /dev/null
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.examples.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);
+ }
+}
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;
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",
@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);
}
}