feat(head-tracking-demo): add compass-ring head tracking demo master
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Wed, 9 Sep 2026 19:07:48 +0000 (22:07 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Wed, 9 Sep 2026 19:07:48 +0000 (22:07 +0300)
- HeadTrackingDemo: eight direction labels surrounding the camera
  so head yaw is immediately visible, plus a floor grid and a
  hint panel (plug in glasses, Scroll Lock recenters)
- register the demo in the launcher list
- document head tracking controls and hot-plug behavior in
  doc/index.org

doc/index.org
src/main/java/eu/svjatoslav/sixth/e3d/examples/head_tracking_demo/HeadTrackingDemo.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/examples/launcher/ApplicationListPanel.java

index 9779076..d45c8c1 100644 (file)
@@ -171,11 +171,22 @@ You should see this:
 | Arrow Right        | Move right (strafe)         |
 | Mouse drag         | Look around (rotate camera) |
 | Mouse scroll wheel | Move up / down              |
+| Scroll Lock        | Recenter head tracking      |
 
 Movement uses physics-based acceleration for smooth, natural
 motion. The faster you're moving, the more acceleration builds up,
 creating an intuitive flying experience.
 
+*Head tracking:* when RayNeo XR glasses are plugged in (USB), every
+demo automatically rotates the camera with your head — the virtual
+world stays fixed in space while you look around. Hot-plug works at
+runtime; plugging mid-demo starts tracking within a couple of seconds,
+unplugging returns to mouse/keyboard control. Hold still for a second
+after plug-in while the gyro calibrates, and press *Scroll Lock* to
+recenter if the view drifts off-center. Disable with
+=-De3d.headtrack=false=. See the *Head tracking* demo for a compass
+scene built to show this off.
+
 Press *F12* in any demo to open the [[https://www3.svjatoslav.eu/projects/sixth-3d/#developer-tools][Developer Tools panel]]. This
 debugging interface provides real-time insight into the rendering
 pipeline: diagnostic toggles, camera position, frustum culling
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/examples/head_tracking_demo/HeadTrackingDemo.java b/src/main/java/eu/svjatoslav/sixth/e3d/examples/head_tracking_demo/HeadTrackingDemo.java
new file mode 100644 (file)
index 0000000..f7d7cab
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Sixth 3D engine demos. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.examples.head_tracking_demo;
+
+import eu.svjatoslav.sixth.e3d.gui.TextPointer;
+import eu.svjatoslav.sixth.e3d.gui.ViewFrame;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas;
+
+import static eu.svjatoslav.sixth.e3d.geometry.Point3D.point;
+
+/**
+ * Head tracking demo for RayNeo XR glasses.
+ *
+ * <p>The camera stands at the center of a compass ring: eight direction
+ * labels surround you at every azimuth. With glasses plugged in, turn
+ * your head — the world stays fixed in space and the labels pan by.
+ * Without glasses, drag the mouse / use arrow keys instead.</p>
+ *
+ * <p>Head tracking is automatic in every engine app (including this
+ * one): the glasses are hot-plug detected, no setup code needed.
+ * Press <b>Scroll Lock</b> to recenter the view to your current head
+ * pose. Disable with {@code -De3d.headtrack=false}.</p>
+ */
+public class HeadTrackingDemo {
+
+    private static final String[] DIRECTIONS =
+            {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
+
+    private static final double RING_RADIUS = 400;
+
+    public static void main(final String[] args) {
+        new HeadTrackingDemo().run();
+    }
+
+    private void run() {
+        final ViewFrame viewFrame = new ViewFrame("Head Tracking Demo");
+        final ViewPanel viewPanel = viewFrame.getViewPanel();
+        final ShapeCollection scene = viewPanel.getRootShapeCollection();
+
+        // camera at the center of the compass ring, looking north (+Z)
+        viewPanel.getCamera().getTransform().setTranslation(
+                point(0, 0, 0));
+
+        addCompassRing(scene);
+        addFloorGrid(scene);
+        addHint(scene);
+
+        viewPanel.repaintDuringNextViewUpdate();
+    }
+
+    /**
+     * Eight direction labels around the camera, facing inward.
+     */
+    private static void addCompassRing(final ShapeCollection scene) {
+        final Color[] colors = {
+                Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN,
+                Color.BLUE, Color.MAGENTA, new Color(255, 165, 0),
+                Color.WHITE,
+        };
+        for (int i = 0; i < DIRECTIONS.length; i++) {
+            final double angle = i * Math.PI / 4;
+            final double x = Math.sin(angle) * RING_RADIUS;
+            final double z = Math.cos(angle) * RING_RADIUS;
+
+            final String label = " " + DIRECTIONS[i] + " ";
+            final Transform transform = new Transform(point(x, 0, z));
+            // yaw the label so its readable face points at the origin;
+            // yaw=angle+PI would show the back face (mirrored text)
+            transform.set(x, 0, z, angle, 0, 0);
+            final TextCanvas canvas = new TextCanvas(transform,
+                    new TextPointer(2, label.length()),
+                    colors[i], new Color(0, 0, 16));
+            canvas.setText(label + "\n" + " ".repeat(label.length()));
+            scene.addShape(canvas);
+        }
+    }
+
+    private static void addFloorGrid(final ShapeCollection scene) {
+        final LineAppearance appearance =
+                new LineAppearance(1, new Color(40, 40, 40));
+        final double size = 1000;
+        final double step = 100;
+        for (double x = -size; x <= size; x += step)
+            scene.addShape(appearance.getLine(
+                    point(x, 200, -size), point(x, 200, size)));
+        for (double z = -size; z <= size; z += step)
+            scene.addShape(appearance.getLine(
+                    point(-size, 200, z), point(size, 200, z)));
+    }
+
+    private static void addHint(final ShapeCollection scene) {
+        final String[] lines = {
+                "  Plug in RayNeo glasses and look around  ",
+                "  Scroll Lock = recenter view           ",
+        };
+        final Transform transform = new Transform(point(0, -80, 150));
+        final TextCanvas hint = new TextCanvas(transform,
+                new TextPointer(lines.length, lines[0].length()),
+                new Color(200, 200, 200), new Color(0, 0, 0));
+        hint.setText(String.join("\n", lines));
+        scene.addShape(hint);
+    }
+}
index 28673e4..22852a1 100644 (file)
@@ -67,6 +67,10 @@ class ApplicationListPanel extends JPanel {
             new DemoEntry("Stereoscopic 3D",
                     "Side-by-side stereo rendering for XR glasses — press SHIFT+F11",
                     new ShowStereoDemo()),
+
+            new DemoEntry("Head tracking",
+                    "Compass ring around you — plug in RayNeo glasses and look around",
+                    new ShowHeadTrackingDemo()),
     };
 
     private static final DemoEntry[] OTHER_DEMOS = {
@@ -418,4 +422,16 @@ class ApplicationListPanel extends JPanel {
             eu.svjatoslav.sixth.e3d.examples.stereo_demo.StereoDemo.main(null);
         }
     }
+
+    private static class ShowHeadTrackingDemo extends AbstractAction {
+        ShowHeadTrackingDemo() {
+            putValue(NAME, "Head tracking");
+        }
+
+        @Override
+        public void actionPerformed(final ActionEvent e) {
+            eu.svjatoslav.sixth.e3d.examples.head_tracking_demo
+                    .HeadTrackingDemo.main(null);
+        }
+    }
 }
\ No newline at end of file