package eu.svjatoslav.sixth.e3d.gui;
import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.math.Matrix3x3;
+import eu.svjatoslav.sixth.e3d.math.Quaternion;
import eu.svjatoslav.sixth.e3d.math.Transform;
/**
* camera between consecutive frames.
*/
private void translateCameraLocationBasedOnMovementVector(int millisecondsPassedSinceLastFrame) {
- final double sinXZ = transform.getRotation().getSinXZ();
- final double cosXZ = transform.getRotation().getCosXZ();
+ final Matrix3x3 m = transform.getRotation().toMatrix();
+
+ final double forwardX = m.m20;
+ final double forwardY = m.m21;
+ final double forwardZ = m.m22;
+
+ final double rightX = m.m00;
+ final double rightY = m.m01;
+ final double rightZ = m.m02;
final Point3D location = transform.getTranslation();
+ final double ms = millisecondsPassedSinceLastFrame;
+
+ location.x += forwardX * movementVector.z * SPEED_MULTIPLIER * ms;
+ location.y += forwardY * movementVector.z * SPEED_MULTIPLIER * ms;
+ location.z += forwardZ * movementVector.z * SPEED_MULTIPLIER * ms;
+
+ location.x += rightX * movementVector.x * SPEED_MULTIPLIER * ms;
+ location.y += rightY * movementVector.x * SPEED_MULTIPLIER * ms;
+ location.z += rightZ * movementVector.x * SPEED_MULTIPLIER * ms;
- location.x -= (float) sinXZ
- * movementVector.z * SPEED_MULTIPLIER
- * millisecondsPassedSinceLastFrame;
- location.z += (float) cosXZ
- * movementVector.z * SPEED_MULTIPLIER
- * millisecondsPassedSinceLastFrame;
-
- location.x += (float) cosXZ
- * movementVector.x * SPEED_MULTIPLIER
- * millisecondsPassedSinceLastFrame;
- location.z += (float) sinXZ
- * movementVector.x * SPEED_MULTIPLIER
- * millisecondsPassedSinceLastFrame;
-
- location.y += movementVector.y * SPEED_MULTIPLIER
- * millisecondsPassedSinceLastFrame;
+ location.y += movementVector.y * SPEED_MULTIPLIER * ms;
}
/**
final double horizontalDist = Math.sqrt(dx * dx + dz * dz);
final double angleYZ = -Math.atan2(dy, horizontalDist);
- transform.setRotation(angleXZ, angleYZ);
+ transform.getRotation().setQuaternion(Quaternion.fromAngles(angleXZ, angleYZ));
}
}
\ No newline at end of file
package eu.svjatoslav.sixth.e3d.gui.humaninput;
import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
import eu.svjatoslav.sixth.e3d.gui.Camera;
import eu.svjatoslav.sixth.e3d.gui.FrameListener;
import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.math.Quaternion;
import eu.svjatoslav.sixth.e3d.math.Rotation;
import java.awt.*;
private Point2D currentMouseLocation;
private boolean mouseMoved;
private boolean mouseWithinWindow = false;
+ private double cameraYaw = 0;
+ private double cameraPitch = 0;
/**
* Creates an input manager attached to the given view panel.
}
private boolean handleMouseDragging() {
+ if (mouseDelta.isZero()) {
+ return false;
+ }
+
+ cameraYaw -= mouseDelta.x / 50.0;
+ cameraPitch -= mouseDelta.y / 50.0;
+
+ cameraPitch = Math.max(-Math.PI / 2 + 0.001,
+ Math.min( Math.PI / 2 - 0.001, cameraPitch));
+
final Camera camera = viewPanel.getCamera();
- Rotation rotation = camera.getTransform().getRotation();
- final double newXZ = rotation.getAngleXZ() - ((float) mouseDelta.x / 50);
- final double newYZ = rotation.getAngleYZ() - ((float) mouseDelta.y / 50);
- camera.getTransform().setRotation(newXZ, newYZ);
+ camera.getTransform().getRotation().setQuaternion(Quaternion.fromAngles(cameraYaw, cameraPitch));
- boolean viewUpdateNeeded = !mouseDelta.isZero();
mouseDelta.zero();
- return viewUpdateNeeded;
+ return true;
}
}
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.math;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+
+/**
+ * A 3x3 matrix for 3D transformations.
+ *
+ * <p>Matrix elements are stored in row-major order:</p>
+ * <pre>
+ * | m00 m01 m02 |
+ * | m10 m11 m12 |
+ * | m20 m21 m22 |
+ * </pre>
+ *
+ * @see Point3D
+ */
+public class Matrix3x3 {
+
+ public double m00;
+ public double m01;
+ public double m02;
+ public double m10;
+ public double m11;
+ public double m12;
+ public double m20;
+ public double m21;
+ public double m22;
+
+ /**
+ * Creates a zero matrix.
+ */
+ public Matrix3x3() {
+ }
+
+ /**
+ * Returns an identity matrix.
+ *
+ * @return a new identity matrix
+ */
+ public static Matrix3x3 identity() {
+ final Matrix3x3 m = new Matrix3x3();
+ m.m00 = 1;
+ m.m11 = 1;
+ m.m22 = 1;
+ return m;
+ }
+
+ /**
+ * Applies this matrix transformation to a point.
+ *
+ * @param in the input point (not modified)
+ * @param out the output point (will be modified)
+ */
+ public void transform(final Point3D in, final Point3D out) {
+ final double x = m00 * in.x + m01 * in.y + m02 * in.z;
+ final double y = m10 * in.x + m11 * in.y + m12 * in.z;
+ final double z = m20 * in.x + m21 * in.y + m22 * in.z;
+ out.x = x;
+ out.y = y;
+ out.z = z;
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.math;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+
+/**
+ * A unit quaternion representing a 3D rotation.
+ *
+ * <p>Quaternions provide a compact representation of rotations that avoids
+ * gimbal lock and enables smooth interpolation (slerp).</p>
+ *
+ * @see Matrix3x3
+ * @see Rotation
+ */
+public class Quaternion {
+
+ public double w;
+ public double x;
+ public double y;
+ public double z;
+
+ /**
+ * Creates a quaternion with the specified components.
+ *
+ * @param w the scalar component
+ * @param x the i component
+ * @param y the j component
+ * @param z the k component
+ */
+ public Quaternion(final double w, final double x, final double y, final double z) {
+ this.w = w;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ /**
+ * Returns the identity quaternion representing no rotation.
+ *
+ * @return the identity quaternion (1, 0, 0, 0)
+ */
+ public static Quaternion identity() {
+ return new Quaternion(1, 0, 0, 0);
+ }
+
+ /**
+ * Creates a quaternion from an axis-angle representation.
+ *
+ * @param axis the rotation axis (must be normalized)
+ * @param angle the rotation angle in radians
+ * @return a quaternion representing the rotation
+ */
+ public static Quaternion fromAxisAngle(final Point3D axis, final double angle) {
+ final double halfAngle = angle / 2;
+ final double s = sin(halfAngle);
+ final double c = cos(halfAngle);
+ return new Quaternion(c, axis.x * s, axis.y * s, axis.z * s);
+ }
+
+ /**
+ * Creates a quaternion from XZ (yaw) and YZ (pitch) Euler angles.
+ *
+ * <p>The rotation is composed as yaw (around Y axis) followed by
+ * pitch (around X axis).</p>
+ *
+ * @param angleXZ the angle around the XZ axis (yaw) in radians
+ * @param angleYZ the angle around the YZ axis (pitch) in radians
+ * @return a quaternion representing the combined rotation
+ */
+ public static Quaternion fromAngles(final double angleXZ, final double angleYZ) {
+ final Quaternion yaw = fromAxisAngle(new Point3D(0, 1, 0), angleXZ);
+ final Quaternion pitch = fromAxisAngle(new Point3D(1, 0, 0), -angleYZ);
+ return pitch.multiply(yaw);
+ }
+
+ /**
+ * Multiplies this quaternion by another (Hamilton product).
+ *
+ * @param other the quaternion to multiply by
+ * @return a new quaternion representing the combined rotation
+ */
+ public Quaternion multiply(final Quaternion other) {
+ return new Quaternion(
+ w * other.w - x * other.x - y * other.y - z * other.z,
+ w * other.x + x * other.w + y * other.z - z * other.y,
+ w * other.y - x * other.z + y * other.w + z * other.x,
+ w * other.z + x * other.y - y * other.x + z * other.w
+ );
+ }
+
+ /**
+ * Normalizes this quaternion to unit length.
+ *
+ * @return this quaternion (for chaining)
+ */
+ public Quaternion normalize() {
+ final double len = Math.sqrt(w * w + x * x + y * y + z * z);
+ if (len > 0) {
+ w /= len;
+ x /= len;
+ y /= len;
+ z /= len;
+ }
+ return this;
+ }
+
+ /**
+ * Converts this quaternion to a 3x3 rotation matrix.
+ *
+ * @return a new matrix representing this rotation
+ */
+ public Matrix3x3 toMatrix3x3() {
+ final Matrix3x3 m = new Matrix3x3();
+
+ m.m00 = 1 - 2 * (y * y + z * z);
+ m.m01 = 2 * (x * y - w * z);
+ m.m02 = 2 * (x * z + w * y);
+
+ m.m10 = 2 * (x * y + w * z);
+ m.m11 = 1 - 2 * (x * x + z * z);
+ m.m12 = 2 * (y * z - w * x);
+
+ m.m20 = 2 * (x * z - w * y);
+ m.m21 = 2 * (y * z + w * x);
+ m.m22 = 1 - 2 * (x * x + y * y);
+
+ return m;
+ }
+
+}
\ No newline at end of file
import eu.svjatoslav.sixth.e3d.geometry.Point3D;
-import static java.lang.Math.cos;
-import static java.lang.Math.sin;
+import static java.lang.Math.atan2;
/**
- * Represents a rotation in 3D space using two Euler angles (XZ and YZ).
+ * Represents a rotation in 3D space using a quaternion internally.
*
- * <p>Angles are stored with precomputed sine and cosine values for efficient
- * repeated rotation operations without recalculating trigonometric functions.</p>
+ * <p>Historically used two Euler angles (XZ and YZ). Now stores a quaternion
+ * for better numerical stability and to avoid gimbal lock.</p>
*
* @see Transform
+ * @see Quaternion
*/
public class Rotation implements Cloneable {
- /**
- * Precomputed sine and cosine of the rotation angles.
- */
- private double s1, c1, s2, c2;
-
- /**
- * The angle of rotation around the XZ axis (yaw).
- */
- private double angleXZ = 0;
-
- /**
- * The angle of rotation around the YZ axis (pitch).
- */
- private double angleYZ = 0;
+ private Quaternion quaternion;
/**
- * Creates a rotation with no rotation (zero angles).
+ * Creates a rotation with no rotation (identity).
*/
public Rotation() {
- computeMultipliers();
+ quaternion = Quaternion.identity();
}
/**
* @param angleYZ the angle around the YZ axis (pitch) in radians
*/
public Rotation(final double angleXZ, final double angleYZ) {
- this.angleXZ = angleXZ;
- this.angleYZ = angleYZ;
- computeMultipliers();
+ quaternion = Quaternion.fromAngles(angleXZ, angleYZ);
}
/**
- * Creates a copy of this rotation with the same angles.
+ * Creates a copy of this rotation with the same orientation.
*
- * @return a new rotation with the same angle values
+ * @return a new rotation with the same quaternion values
*/
@Override
public Rotation clone() {
- return new Rotation(angleXZ, angleYZ);
+ final Rotation r = new Rotation();
+ r.quaternion = new Quaternion(quaternion.w, quaternion.x, quaternion.y, quaternion.z);
+ return r;
}
/**
- * Recomputes the sine and cosine values from the current angles.
- */
- private void computeMultipliers() {
- s1 = sin(angleXZ);
- c1 = cos(angleXZ);
-
- s2 = sin(angleYZ);
- c2 = cos(angleYZ);
- }
-
- /**
- * Rotates a point around the origin using this rotation's angles.
+ * Rotates a point around the origin using this rotation.
*
* @param point3d the point to rotate (modified in place)
*/
public void rotate(final Point3D point3d) {
- // Rotate around the XZ axis
- final double z1 = (point3d.z * c1) - (point3d.x * s1);
- point3d.x = (point3d.z * s1) + (point3d.x * c1);
-
- // Rotate around the YZ axis
- point3d.z = (z1 * c2) - (point3d.y * s2);
- point3d.y = (z1 * s2) + (point3d.y * c2);
+ toMatrix().transform(point3d, point3d);
}
/**
- * Adds the specified angles to this rotation and updates the trigonometric values.
+ * 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) {
- this.angleXZ += angleXZ;
- this.angleYZ += angleYZ;
- computeMultipliers();
+ final Quaternion delta = Quaternion.fromAngles(angleXZ, angleYZ);
+ quaternion = delta.multiply(quaternion);
}
/**
- * Sets the rotation angles and recomputes the trigonometric values.
+ * 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) {
- this.angleXZ = angleXZ;
- this.angleYZ = angleYZ;
- computeMultipliers();
+ quaternion = Quaternion.fromAngles(angleXZ, angleYZ);
}
/**
- * Copies the angles from another rotation into this rotation.
+ * Copies the rotation from another rotation.
*
- * @param rotation the rotation to copy angles from
+ * @param rotation the rotation to copy from
*/
- public void setAngles(Rotation rotation) {
- this.angleXZ = rotation.angleXZ;
- this.angleYZ = rotation.angleYZ;
- computeMultipliers();
+ 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.
+ *
+ * @param q the quaternion to set
+ */
+ public void setQuaternion(final Quaternion q) {
+ quaternion = new Quaternion(q.w, q.x, q.y, q.z);
+ }
+
+ /**
+ * Returns the internal quaternion.
+ *
+ * @return the quaternion (not a copy)
+ */
+ public Quaternion getQuaternion() {
+ 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() {
- return angleXZ;
+ 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() {
- return angleYZ;
- }
-
- /**
- * Returns the precomputed sine of the XZ angle.
- *
- * @return sin(angleXZ)
- */
- public double getSinXZ() {
- return s1;
+ final Matrix3x3 m = toMatrix();
+ return atan2(-m.m21, m.m11);
}
/**
- * Returns the precomputed cosine of the XZ angle.
+ * Converts this rotation to a 3x3 transformation matrix.
*
- * @return cos(angleXZ)
+ * @return a matrix representing this rotation
*/
- public double getCosXZ() {
- return c1;
+ public Matrix3x3 toMatrix() {
+ return quaternion.toMatrix3x3();
}
}
\ No newline at end of file
* @param point the point to transform (modified in place)
*/
public void transform(final Point3D point) {
- rotation.rotate(point);
+ rotation.toMatrix().transform(point, point);
point.add(translation);
}
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.math;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class QuaternionTest {
+
+ @Test
+ public void testFromAnglesMatchesRotation() {
+ final Rotation rotation = new Rotation(0.5, 0.3);
+ final Matrix3x3 rotationMatrix = rotation.toMatrix();
+
+ final Quaternion quaternion = Quaternion.fromAngles(0.5, 0.3);
+ final Matrix3x3 quaternionMatrix = quaternion.toMatrix3x3();
+
+ final double epsilon = 0.0001;
+ assertEquals(rotationMatrix.m00, quaternionMatrix.m00, epsilon);
+ assertEquals(rotationMatrix.m01, quaternionMatrix.m01, epsilon);
+ assertEquals(rotationMatrix.m02, quaternionMatrix.m02, epsilon);
+ assertEquals(rotationMatrix.m10, quaternionMatrix.m10, epsilon);
+ assertEquals(rotationMatrix.m11, quaternionMatrix.m11, epsilon);
+ assertEquals(rotationMatrix.m12, quaternionMatrix.m12, epsilon);
+ assertEquals(rotationMatrix.m20, quaternionMatrix.m20, epsilon);
+ assertEquals(rotationMatrix.m21, quaternionMatrix.m21, epsilon);
+ assertEquals(rotationMatrix.m22, quaternionMatrix.m22, epsilon);
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.math;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class RotationMatrixTest {
+
+ @Test
+ public void testToMatrixMatchesRotate() {
+ final Rotation rotation = new Rotation(0.5, 0.3);
+ final Point3D original = new Point3D(1, 2, 3);
+
+ final Point3D viaRotate = new Point3D(original);
+ rotation.rotate(viaRotate);
+
+ final Point3D viaMatrix = new Point3D();
+ rotation.toMatrix().transform(original, viaMatrix);
+
+ final double epsilon = 0.0001;
+ assertEquals(viaRotate.x, viaMatrix.x, epsilon);
+ assertEquals(viaRotate.y, viaMatrix.y, epsilon);
+ assertEquals(viaRotate.z, viaMatrix.z, epsilon);
+ }
+
+}
\ No newline at end of file