| 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
--- /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.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);
+ }
+}