From: Svjatoslav Agejenko Date: Fri, 20 Mar 2026 20:51:50 +0000 (+0200) Subject: refactor(math): simplify Rotation API to use quaternions exclusively X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=2ccd3edafc01d102a24de0cf59903d5346a48e2e;p=sixth-3d.git refactor(math): simplify Rotation API to use quaternions exclusively --- diff --git a/TODO.org b/TODO.org index 25051af..d54e650 100644 --- 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 index 7094240..0000000 Binary files a/doc/example.png and /dev/null differ diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java index f5542e6..bafdb83 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java @@ -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. * *

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.

* *

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); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java index 236be0d..8e529d0 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java @@ -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. * - *

Historically used two Euler angles (XZ and YZ). Now stores a quaternion - * for better numerical stability and to avoid gimbal lock.

+ *

Quaternions provide smooth interpolation and avoid gimbal lock + * compared to Euler angles.

* * @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. - * - *

This is a compatibility shim that extracts the angle from the quaternion. - * May not be accurate for extreme pitch angles.

- * - * @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. - * - *

This is a compatibility shim that extracts the angle from the quaternion. - * May not be accurate for extreme yaw angles.

- * - * @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. * diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java index 021fe1a..047308a 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java @@ -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 diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java index 12d149e..d585576 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java @@ -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); } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java index f0f5184..d751a84 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java @@ -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); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java index 07be4a3..c075580 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java @@ -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(); diff --git a/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java b/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java index 70d0488..831ba29 100644 --- a/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java +++ b/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java @@ -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); diff --git a/src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java b/src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java index d0d14e6..79cb7c4 100644 --- a/src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java +++ b/src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java @@ -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);