refactor(math): simplify Rotation API to use quaternions exclusively
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Fri, 20 Mar 2026 20:51:50 +0000 (22:51 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Fri, 20 Mar 2026 20:52:22 +0000 (22:52 +0200)
TODO.org
doc/example.png [deleted file]
src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java
src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java
src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java
src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java
src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java

index 25051af..d54e650 100644 (file)
--- a/TODO.org
+++ b/TODO.org
@@ -93,3 +93,5 @@ Occurs when text is forward-oriented.
   http://blog.rogach.org/2015/08/how-to-create-your-own-simple-3d-render.html
 
 + Improve triangulation. Read: https://ianthehenry.com/posts/delaunay/
+
+** Fix camera rotation for voxel raytracer
\ No newline at end of file
diff --git a/doc/example.png b/doc/example.png
deleted file mode 100644 (file)
index 7094240..0000000
Binary files a/doc/example.png and /dev/null differ
index f5542e6..bafdb83 100644 (file)
@@ -13,7 +13,7 @@ import eu.svjatoslav.sixth.e3d.math.Transform;
  * Represents the viewer's camera in the 3D world, with position, orientation, and movement.
  *
  * <p>The camera is the user's "eyes" in the 3D scene. It has a position (location),
- * a looking direction (defined by XZ and YZ angles), and a movement system with
+ * a looking direction (defined by a quaternion), and a movement system with
  * velocity, acceleration, and friction for smooth camera navigation.</p>
  *
  * <p>By default, the user can navigate using arrow keys (handled by
@@ -28,8 +28,8 @@ import eu.svjatoslav.sixth.e3d.math.Transform;
  * // Set camera position
  * camera.getTransform().setTranslation(new Point3D(0, -50, -200));
  *
- * // Set camera orientation (radians)
- * camera.getTransform().setRotation(0, 0);  // angleXZ, angleYZ
+ * // Set camera orientation using a quaternion
+ * camera.getTransform().getRotation().setQuaternion(Quaternion.fromAngles(0.5, -0.3));
  *
  * // Copy camera state from another camera
  * Camera snapshot = new Camera(camera);
index 236be0d..8e529d0 100644 (file)
@@ -6,13 +6,11 @@ package eu.svjatoslav.sixth.e3d.math;
 
 import eu.svjatoslav.sixth.e3d.geometry.Point3D;
 
-import static java.lang.Math.atan2;
-
 /**
- * Represents a rotation in 3D space using a quaternion internally.
+ * Represents a rotation in 3D space using a quaternion.
  *
- * <p>Historically used two Euler angles (XZ and YZ). Now stores a quaternion
- * for better numerical stability and to avoid gimbal lock.</p>
+ * <p>Quaternions provide smooth interpolation and avoid gimbal lock
+ * compared to Euler angles.</p>
  *
  * @see Transform
  * @see Quaternion
@@ -28,16 +26,6 @@ public class Rotation implements Cloneable {
         quaternion = Quaternion.identity();
     }
 
-    /**
-     * Creates a rotation with the specified Euler angles.
-     *
-     * @param angleXZ the angle around the XZ axis (yaw) in radians
-     * @param angleYZ the angle around the YZ axis (pitch) in radians
-     */
-    public Rotation(final double angleXZ, final double angleYZ) {
-        quaternion = Quaternion.fromAngles(angleXZ, angleYZ);
-    }
-
     /**
      * Creates a copy of this rotation with the same orientation.
      *
@@ -59,41 +47,6 @@ public class Rotation implements Cloneable {
         toMatrix().transform(point3d, point3d);
     }
 
-    /**
-     * Adds the specified angles to this rotation.
-     *
-     * @param angleXZ the angle to add around the XZ axis in radians
-     * @param angleYZ the angle to add around the YZ axis in radians
-     */
-    public void rotate(final double angleXZ, final double angleYZ) {
-        final Quaternion delta = Quaternion.fromAngles(angleXZ, angleYZ);
-        quaternion = delta.multiply(quaternion);
-    }
-
-    /**
-     * Sets the rotation angles.
-     *
-     * @param angleXZ the angle around the XZ axis (yaw) in radians
-     * @param angleYZ the angle around the YZ axis (pitch) in radians
-     */
-    public void setAngles(final double angleXZ, final double angleYZ) {
-        quaternion = Quaternion.fromAngles(angleXZ, angleYZ);
-    }
-
-    /**
-     * Copies the rotation from another rotation.
-     *
-     * @param rotation the rotation to copy from
-     */
-    public void setAngles(final Rotation rotation) {
-        quaternion = new Quaternion(
-            rotation.quaternion.w,
-            rotation.quaternion.x,
-            rotation.quaternion.y,
-            rotation.quaternion.z
-        );
-    }
-
     /**
      * Sets the rotation from a quaternion.
      *
@@ -112,32 +65,6 @@ public class Rotation implements Cloneable {
         return quaternion;
     }
 
-    /**
-     * Returns the angle around the XZ axis (yaw) in radians.
-     *
-     * <p>This is a compatibility shim that extracts the angle from the quaternion.
-     * May not be accurate for extreme pitch angles.</p>
-     *
-     * @return the XZ angle
-     */
-    public double getAngleXZ() {
-        final Matrix3x3 m = toMatrix();
-        return atan2(m.m02, m.m00);
-    }
-
-    /**
-     * Returns the angle around the YZ axis (pitch) in radians.
-     *
-     * <p>This is a compatibility shim that extracts the angle from the quaternion.
-     * May not be accurate for extreme yaw angles.</p>
-     *
-     * @return the YZ angle
-     */
-    public double getAngleYZ() {
-        final Matrix3x3 m = toMatrix();
-        return atan2(-m.m21, m.m11);
-    }
-
     /**
      * Converts this rotation to a 3x3 transformation matrix.
      *
index 021fe1a..047308a 100755 (executable)
@@ -44,17 +44,17 @@ public class Transform implements Cloneable {
     }
 
     /**
-     * Creates a transform with the specified translation and rotation angles.
+     * Creates a transform with the specified translation and rotation from Euler angles.
      *
      * @param translation the translation
      * @param angleXZ     the angle around the XZ axis (yaw) in radians
      * @param angleYZ     the angle around the YZ axis (pitch) in radians
+     * @return a new transform with the specified translation and rotation
      */
-    public Transform(final Point3D translation, final double angleXZ,
-                     final double angleYZ) {
-
-        this.translation = translation;
-        rotation = new Rotation(angleXZ, angleYZ);
+    public static Transform fromAngles(final Point3D translation, final double angleXZ, final double angleYZ) {
+        final Transform t = new Transform(translation);
+        t.rotation.setQuaternion(Quaternion.fromAngles(angleXZ, angleYZ));
+        return t;
     }
 
     /**
@@ -96,7 +96,7 @@ public class Transform implements Cloneable {
         return translation;
     }
 
-   /**
+    /**
      * Applies this transform to a point: rotation followed by translation.
      *
      * @param point the point to transform (modified in place)
@@ -106,16 +106,6 @@ public class Transform implements Cloneable {
         point.add(translation);
     }
 
-    /**
-     * Sets the rotation angles for this transform.
-     *
-     * @param angleXZ the angle around the XZ axis (yaw) in radians
-     * @param angleYZ the angle around the YZ axis (pitch) in radians
-     */
-    public void setRotation(final double angleXZ, final double angleYZ) {
-        rotation.setAngles(angleXZ, angleYZ);
-    }
-
     /**
      * Sets the translation for this transform by copying the values from the given point.
      *
@@ -127,4 +117,4 @@ public class Transform implements Cloneable {
         this.translation.z = translation.z;
     }
 
-}
+}
\ No newline at end of file
index 12d149e..d585576 100644 (file)
@@ -6,7 +6,7 @@ package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer;
 
 import eu.svjatoslav.sixth.e3d.geometry.Point3D;
 import eu.svjatoslav.sixth.e3d.gui.Camera;
-import eu.svjatoslav.sixth.e3d.math.Rotation;
+import eu.svjatoslav.sixth.e3d.math.Matrix3x3;
 
 import static eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.RaytracingCamera.SIZE;
 
@@ -27,7 +27,6 @@ public class CameraView {
      * @param zoom   the zoom level (scales the view frustum)
      */
     public CameraView(final Camera camera, final double zoom) {
-        // compute camera view coordinates as if camera is at (0,0,0) and look at (0,0,1)
         final float viewAngle = (float) .6;
         cameraCenter = new Point3D();
         topLeft = new Point3D(0, 0, SIZE).rotate(-viewAngle, -viewAngle);
@@ -35,13 +34,21 @@ public class CameraView {
         bottomLeft = new Point3D(0, 0, SIZE).rotate(-viewAngle, viewAngle);
         bottomRight = new Point3D(0, 0, SIZE).rotate(viewAngle, viewAngle);
 
-        Rotation rotation = camera.getTransform().getRotation();
-        topLeft.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ());
-        topRight.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ());
-        bottomLeft.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ());
-        bottomRight.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ());
+        final Matrix3x3 m = camera.getTransform().getRotation().toMatrix();
+        final Point3D temp = new Point3D();
+        
+        temp.clone(topLeft);
+        m.transform(temp, topLeft);
+        
+        temp.clone(topRight);
+        m.transform(temp, topRight);
+        
+        temp.clone(bottomLeft);
+        m.transform(temp, bottomLeft);
+        
+        temp.clone(bottomRight);
+        m.transform(temp, bottomRight);
 
-        // place camera view at camera location
         camera.getTransform().getTranslation().clone().scaleDown(zoom).addTo(cameraCenter, topLeft, topRight, bottomLeft, bottomRight);
     }
 
index f0f5184..d751a84 100755 (executable)
@@ -6,7 +6,7 @@ package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer;
 
 import eu.svjatoslav.sixth.e3d.geometry.Point3D;
 import eu.svjatoslav.sixth.e3d.gui.Camera;
-import eu.svjatoslav.sixth.e3d.math.Rotation;
+import eu.svjatoslav.sixth.e3d.math.Matrix3x3;
 import eu.svjatoslav.sixth.e3d.math.Transform;
 import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
@@ -79,11 +79,28 @@ public class RaytracingCamera extends TexturedRectangle {
         bottomLeft.rotate(cameraCenter, -viewAngle, viewAngle);
         bottomRight.rotate(cameraCenter, viewAngle, viewAngle);
 
-        Rotation rotation = camera.getTransform().getRotation();
-        topLeft.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ());
-        topRight.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ());
-        bottomLeft.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ());
-        bottomRight.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ());
+        final Matrix3x3 m = camera.getTransform().getRotation().toMatrix();
+        final Point3D temp = new Point3D();
+        
+        temp.clone(topLeft);
+        temp.subtract(cameraCenter);
+        m.transform(temp, topLeft);
+        topLeft.add(cameraCenter);
+        
+        temp.clone(topRight);
+        temp.subtract(cameraCenter);
+        m.transform(temp, topRight);
+        topRight.add(cameraCenter);
+        
+        temp.clone(bottomLeft);
+        temp.subtract(cameraCenter);
+        m.transform(temp, bottomLeft);
+        bottomLeft.add(cameraCenter);
+        
+        temp.clone(bottomRight);
+        temp.subtract(cameraCenter);
+        m.transform(temp, bottomRight);
+        bottomRight.add(cameraCenter);
 
         final Color cameraColor = new Color(255, 255, 0, 255);
         final LineAppearance appearance = new LineAppearance(2, cameraColor);
index 07be4a3..c075580 100755 (executable)
@@ -106,7 +106,8 @@ public class ShapeCollection {
 
         final Camera camera = viewPanel.getCamera();
 
-        cameraRotationTransform.getRotation().setAngles(camera.getTransform().getRotation());
+        cameraRotationTransform.getRotation().setQuaternion(
+                camera.getTransform().getRotation().getQuaternion());
         transformStack.addTransform(cameraRotationTransform);
 
         final Point3D cameraLocation = camera.getTransform().getTranslation();
index 70d0488..831ba29 100644 (file)
@@ -12,7 +12,8 @@ public class QuaternionTest {
 
     @Test
     public void testFromAnglesMatchesRotation() {
-        final Rotation rotation = new Rotation(0.5, 0.3);
+        final Rotation rotation = new Rotation();
+        rotation.setQuaternion(Quaternion.fromAngles(0.5, 0.3));
         final Matrix3x3 rotationMatrix = rotation.toMatrix();
 
         final Quaternion quaternion = Quaternion.fromAngles(0.5, 0.3);
index d0d14e6..79cb7c4 100644 (file)
@@ -13,7 +13,8 @@ public class RotationMatrixTest {
 
     @Test
     public void testToMatrixMatchesRotate() {
-        final Rotation rotation = new Rotation(0.5, 0.3);
+        final Rotation rotation = new Rotation();
+        rotation.setQuaternion(Quaternion.fromAngles(0.5, 0.3));
         final Point3D original = new Point3D(1, 2, 3);
 
         final Point3D viaRotate = new Point3D(original);