From 1c79fa6a3fe9b0aec1aa6c020b9b9b4dbe14a411 Mon Sep 17 00:00:00 2001 From: Bukkit/Spigot Date: Thu, 14 Feb 2019 21:28:20 +0100 Subject: [PATCH] Introduce rotation methods to the Vector class By: Bjarne Koll --- .../src/main/java/org/bukkit/util/Vector.java | 143 ++++++++++++++++++ .../test/java/org/bukkit/util/VectorTest.java | 117 ++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 paper-api/src/test/java/org/bukkit/util/VectorTest.java diff --git a/paper-api/src/main/java/org/bukkit/util/Vector.java b/paper-api/src/main/java/org/bukkit/util/Vector.java index 068361f58c..e1d8d2b340 100644 --- a/paper-api/src/main/java/org/bukkit/util/Vector.java +++ b/paper-api/src/main/java/org/bukkit/util/Vector.java @@ -1,5 +1,6 @@ package org.bukkit.util; +import com.google.common.base.Preconditions; import java.util.LinkedHashMap; import java.util.Map; import java.util.Random; @@ -373,6 +374,148 @@ public class Vector implements Cloneable, ConfigurationSerializable { return (NumberConversions.square(origin.x - x) + NumberConversions.square(origin.y - y) + NumberConversions.square(origin.z - z)) <= NumberConversions.square(radius); } + /** + * Returns if a vector is normalized + * + * @return whether the vector is normalised + */ + public boolean isNormalized() { + return Math.abs(this.lengthSquared() - 1) < getEpsilon(); + } + + /** + * Rotates the vector around the x axis. + *

+ * This piece of math is based on the standard rotation matrix for vectors + * in three dimensional space. This matrix can be found here: + * Rotation + * Matrix. + * + * @param angle the angle to rotate the vector about. This angle is passed + * in radians + * @return the same vector + */ + public Vector rotateAroundX(double angle) { + double angleCos = Math.cos(angle); + double angleSin = Math.sin(angle); + + double y = angleCos * getY() - angleSin * getZ(); + double z = angleSin * getY() + angleCos * getZ(); + return setY(y).setZ(z); + } + + /** + * Rotates the vector around the y axis. + *

+ * This piece of math is based on the standard rotation matrix for vectors + * in three dimensional space. This matrix can be found here: + * Rotation + * Matrix. + * + * @param angle the angle to rotate the vector about. This angle is passed + * in radians + * @return the same vector + */ + public Vector rotateAroundY(double angle) { + double angleCos = Math.cos(angle); + double angleSin = Math.sin(angle); + + double x = angleCos * getX() + angleSin * getZ(); + double z = -angleSin * getX() + angleCos * getZ(); + return setX(x).setZ(z); + } + + /** + * Rotates the vector around the z axis + *

+ * This piece of math is based on the standard rotation matrix for vectors + * in three dimensional space. This matrix can be found here: + * Rotation + * Matrix. + * + * @param angle the angle to rotate the vector about. This angle is passed + * in radians + * @return the same vector + */ + public Vector rotateAroundZ(double angle) { + double angleCos = Math.cos(angle); + double angleSin = Math.sin(angle); + + double x = angleCos * getX() - angleSin * getY(); + double y = angleSin * getX() + angleCos * getY(); + return setX(x).setY(y); + } + + /** + * Rotates the vector around a given arbitrary axis in 3 dimensional space. + * + *

+ * Rotation will follow the general Right-Hand-Rule, which means rotation + * will be counterclockwise when the axis is pointing towards the observer. + *

+ * This method will always make sure the provided axis is a unit vector, to + * not modify the length of the vector when rotating. If you are experienced + * with the scaling of a non-unit axis vector, you can use + * {@link Vector#rotateAroundNonUnitAxis(Vector, double)}. + * + * @param axis the axis to rotate the vector around. If the passed vector is + * not of length 1, it gets copied and normalized before using it for the + * rotation. Please use {@link Vector#normalize()} on the instance before + * passing it to this method + * @param angle the angle to rotate the vector around the axis + * @return the same vector + * @throws IllegalArgumentException if the provided axis vector instance is + * null + */ + public Vector rotateAroundAxis(Vector axis, double angle) throws IllegalArgumentException { + Preconditions.checkArgument(axis != null, "The provided axis vector was null"); + + return rotateAroundNonUnitAxis(axis.isNormalized() ? axis : axis.clone().normalize(), angle); + } + + /** + * Rotates the vector around a given arbitrary axis in 3 dimensional space. + * + *

+ * Rotation will follow the general Right-Hand-Rule, which means rotation + * will be counterclockwise when the axis is pointing towards the observer. + *

+ * Note that the vector length will change accordingly to the axis vector + * length. If the provided axis is not a unit vector, the rotated vector + * will not have its previous length. The scaled length of the resulting + * vector will be related to the axis vector. If you are not perfectly sure + * about the scaling of the vector, use + * {@link Vector#rotateAroundAxis(Vector, double)} + * + * @param axis the axis to rotate the vector around. + * @param angle the angle to rotate the vector around the axis + * @return the same vector + * @throws IllegalArgumentException if the provided axis vector instance is + * null + */ + public Vector rotateAroundNonUnitAxis(Vector axis, double angle) throws IllegalArgumentException { + Preconditions.checkArgument(axis != null, "The provided axis vector was null"); + + double x = getX(), y = getY(), z = getZ(); + double x2 = axis.getX(), y2 = axis.getY(), z2 = axis.getZ(); + + double cosTheta = Math.cos(angle); + double sinTheta = Math.sin(angle); + double dotProduct = this.dot(axis); + + double xPrime = x2 * dotProduct * (1d - cosTheta) + + x * cosTheta + + (-z2 * y + y2 * z) * sinTheta; + double yPrime = y2 * dotProduct * (1d - cosTheta) + + y * cosTheta + + (z2 * x - x2 * z) * sinTheta; + double zPrime = z2 * dotProduct * (1d - cosTheta) + + z * cosTheta + + (-y2 * x + x2 * y) * sinTheta; + + return setX(xPrime).setY(yPrime).setZ(zPrime); + } + /** * Gets the X component. * diff --git a/paper-api/src/test/java/org/bukkit/util/VectorTest.java b/paper-api/src/test/java/org/bukkit/util/VectorTest.java new file mode 100644 index 0000000000..6170f28c66 --- /dev/null +++ b/paper-api/src/test/java/org/bukkit/util/VectorTest.java @@ -0,0 +1,117 @@ +package org.bukkit.util; + +import org.bukkit.block.BlockFace; +import org.junit.Test; +import static org.junit.Assert.*; + +public class VectorTest { + + @Test + public void testNormalisedVectors() { + assertFalse(new Vector(1, 0, 0).multiply(1.1).isNormalized()); + + assertTrue(new Vector(1, 1, 1).normalize().isNormalized()); + assertTrue(new Vector(1, 0, 0).isNormalized()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNullVectorAxis() { + new Vector(0, 1, 0).rotateAroundAxis(null, Math.PI); + } + + @Test + public void testBypassingAxisVector() { + new Vector(0, 1, 0).rotateAroundNonUnitAxis(new Vector(1, 1, 1), Math.PI); // This will result some weird result, but there may be some use for it for some people + } + + @Test + public void testResizeAxis() { + Vector axis = new Vector(0, 10, 0); + assertEquals(BlockFace.EAST.getDirection().rotateAroundAxis(axis, Math.PI * 0.5), BlockFace.NORTH.getDirection()); + } + + /** + * As west to east are the x axis in Minecraft, rotating around it from up + * should lead to up -> south -> down -> north. + */ + @Test + public void testRotationAroundX() { + Vector vector = BlockFace.UP.getDirection(); + assertEquals(BlockFace.SOUTH.getDirection(), vector.clone().rotateAroundX(Math.PI * 0.5)); // Should rotate around x axis for 1/4 of a circle. + assertEquals(BlockFace.DOWN.getDirection(), vector.clone().rotateAroundX(Math.PI * 1.0)); // Should rotate around x axis for 2/4 of a circle. + assertEquals(BlockFace.NORTH.getDirection(), vector.clone().rotateAroundX(Math.PI * 1.5)); // Should rotate around x axis for 3/4 of a circle. + assertEquals(BlockFace.UP.getDirection(), vector.clone().rotateAroundX(Math.PI * 2.0)); // Should rotate around x axis for 4/4 of a circle. + } + + /** + * As up to down are the y axis in Minecraft, rotating around it from up + * should lead to east (positive x) -> south -> west -> north. + */ + @Test + public void testRotationAroundY() { + Vector vector = BlockFace.EAST.getDirection(); + assertEquals(BlockFace.NORTH.getDirection(), vector.clone().rotateAroundY(Math.PI * 0.5)); // Should rotate around x axis for 1/4 of a circle. + assertEquals(BlockFace.WEST.getDirection(), vector.clone().rotateAroundY(Math.PI * 1.0)); // Should rotate around x axis for 2/4 of a circle. + assertEquals(BlockFace.SOUTH.getDirection(), vector.clone().rotateAroundY(Math.PI * 1.5)); // Should rotate around x axis for 3/4 of a circle. + assertEquals(BlockFace.EAST.getDirection(), vector.clone().rotateAroundY(Math.PI * 2.0)); // Should rotate around x axis for 4/4 of a circle. + } + + /** + * As up to down are the y axis in Minecraft, rotating around it from up + * should lead to east (positive x) -> south -> west -> north. + */ + @Test + public void testRotationAroundYUsingCustomAxis() { + Vector vector = BlockFace.EAST.getDirection(); + Vector axis = BlockFace.UP.getDirection(); + assertEquals(BlockFace.NORTH.getDirection(), vector.clone().rotateAroundAxis(axis, Math.PI * 0.5)); // Should rotate around x axis for 1/4 of a circle. + assertEquals(BlockFace.WEST.getDirection(), vector.clone().rotateAroundAxis(axis, Math.PI * 1.0)); // Should rotate around x axis for 2/4 of a circle. + assertEquals(BlockFace.SOUTH.getDirection(), vector.clone().rotateAroundAxis(axis, Math.PI * 1.5)); // Should rotate around x axis for 3/4 of a circle. + assertEquals(BlockFace.EAST.getDirection(), vector.clone().rotateAroundAxis(axis, Math.PI * 2.0)); // Should rotate around x axis for 4/4 of a circle. + } + + /** + * As south to north are the z axis in Minecraft, rotating around it from up + * should lead to up (positive y) -> west -> down -> east. + */ + @Test + public void testRotationAroundZ() { + Vector vector = BlockFace.UP.getDirection(); + assertEquals(BlockFace.WEST.getDirection(), vector.clone().rotateAroundZ(Math.PI * 0.5)); // Should rotate around x axis for 1/4 of a circle. + assertEquals(BlockFace.DOWN.getDirection(), vector.clone().rotateAroundZ(Math.PI * 1.0)); // Should rotate around x axis for 2/4 of a circle. + assertEquals(BlockFace.EAST.getDirection(), vector.clone().rotateAroundZ(Math.PI * 1.5)); // Should rotate around x axis for 3/4 of a circle. + assertEquals(BlockFace.UP.getDirection(), vector.clone().rotateAroundZ(Math.PI * 2.0)); // Should rotate around x axis for 4/4 of a circle. + } + + @Test + public void testRotationAroundAxis() { + Vector axis = new Vector(1, 0, 1); + assertEquals(new Vector(0, 1, 0).rotateAroundNonUnitAxis(axis, Math.PI * 0.5), new Vector(-1, 0, 1)); + } + + @Test + public void testRotationAroundAxisNonUnit() { + Vector axis = new Vector(0, 2, 0); + Vector v = BlockFace.EAST.getDirection(); + + assertEquals(v.rotateAroundNonUnitAxis(axis, Math.PI * 0.5), BlockFace.NORTH.getDirection().multiply(2)); + } + + /** + * This will be a bit tricky to prove so we will try to simply see if the + * vectors have correct angle to each other This will work with any two + * vectors, as the rotation will keep the angle the same. + */ + @Test + public void testRotationAroundCustomAngle() { + Vector axis = new Vector(-30, 1, 2000).normalize(); + Vector v = new Vector(53, 12, 98); + + float a = v.angle(axis); + double stepSize = Math.PI / 21; + for (int i = 0; i < 42; i++) { + v.rotateAroundAxis(axis, stepSize); + assertEquals(a, v.angle(axis), Vector.getEpsilon()); + } + } +}