To be able to efficiently calculate sine/cosine of an angle in minecraft, the game uses a lookup table with 65536 entries defined as array[n] = sin(2 * pi * n / 65536) for 0 <= n <= 65535. The players yaw is never wrapped to [-180,180] or similar ranges during gameplay, so we require a map from any value to {0, …, 65535} to then be able to use the lookup value. Optimally we would want this mapping to have the following properties:
Periodic with period 2*pi, i.e. for any angle outside of (-pi, pi] we want the resulting lookup-index to be identical to the index obtained by first wrapping to (-pi, pi] and then getting the lookup-index (not including errors from floating point precision).
Mirror symmetry along cardinal axes, i.e. lookup_idx(-a) = 65536 - lookup_idx(a) for 0 <= a <= pi and similarly for the other cardinal axes.
Both of these are not satisfied in the current implementation and it can lead to some very hard to predict behaviour when trying to do precise movement calculations.
Since clearly showing the impact of this without a mod to increase decimal precision of the players yaw is difficult, I will instead refer to my professionally made drawing attached in current.png to explain what the current implementation does and how it affects movement.
First the game takes your angle multiplied by 65536/(2*pi) (the angle translated from radians into “angle units”) and casts it to a long. The important detail to note here is that casting to a long rounds down for positive numbers and up for negatives, so if your internal yaw is exactly -180 (e.g. by doing /tp @s ~ ~ ~ -180 ~) and you do a 360° turn in positive direction, every angle will essentially be offset by 1.
Since the code does casting instead of rounding, the second aforementioned property is also not satisified except for the sole case of the Z-axis symmetry when your internal yaw lies in [-180, 180], since everything gets rounded towards 0.
Another big issue is that the current way of getting the index for the lookup table for calculating the cosine adds an integer to a double before casting instead of after:
(int)((long)(i * 10430.378350470453 + 16384.0) & 65535L)This makes it possible for the index of sine and cosine to not differ by 16384 exactly, making it possible to move faster or slower when facing very specific angles. This doesn’t happen by adding 16386 after casting.
To fix these issues I’d propose to use a form of rounding instead of simply casting down/up depending on the sign of the angle. We can easily use the symmetric properties of sine and floating point arithmetic to eliminate all negative angles, then rounding is equivalent to the faster (long) (x + 0.5). This would result in behaviour as shown in improved.png (shown with NUM_ANGLES = 8).
Additionally, one could store 4 times the sine values in the same amount of memory by using the symmetries of the sine function. A complete implementation could then look something like this:
public static final float[] SIN;
public static final int NUM_ANGLES = 262144;
public static final int HALF_NUM_ANGLES = NUM_ANGLES / 2;
public static final int QUARTER_NUM_ANGLES = NUM_ANGLES / 4;
public static final double ANGLES_PER_RAD = NUM_ANGLES / (2 * Math.PI);
public static float sin(double angle) {
boolean negative = angle < 0.0;
if (negative) {
angle = -angle;
}
int index = (int) ((long) (angle * ANGLES_PER_RAD + 0.5) & (long) (NUM_ANGLES - 1));
return negative ? -sinLookup(index) : sinLookup(index);
}
public static float cos(double angle) {
if (angle < 0.0) {
angle = -angle;
}
int index = (int) (((long) (angle * ANGLES_PER_RAD + 0.5) +
(long) (QUARTER_NUM_ANGLES)) & (long) (NUM_ANGLES - 1));
return sinLookup(index);
}
private static float sinLookup(int index) {
int quadrant = index >>> 16; // or 14 if NUM_ANGLES = 65536
int offset = index & 0xFFFF; // or 0x3FFF if NUM_ANGLES = 65536
return switch (quadrant) {
case 0 -> SIN[offset];
case 1 -> SIN[QUARTER_NUM_ANGLES - offset];
case 2 -> -SIN[offset];
default -> -SIN[QUARTER_NUM_ANGLES - offset];
};
}
static {
float[] sinArray = new float[QUARTER_NUM_ANGLES + 1];
for (int i = 0; i < QUARTER_NUM_ANGLES; i++) {
sinArray[i] = (float)Math.sin((double) i / ANGLES_PER_RAD);
}
sinArray[QUARTER_NUM_ANGLES] = 1.0F;
SIN = sinArray;
}I think an increase from 65536 different angles to 262144 would be very natural at this point and I hope this report might offer a good opportunity for that.
After some benchmarking these new trig methods seem to run at about half the speed than the previously used ones (about 2ns vs. 1ns on my machine), which is mostly due to the sine symmetry memory optimization. Of course this is one of the most frequently called methods in the game and thus there are very valid reasons against this change. Alternatively one could just not do any of these optimizations and reuse the vanilla SIN array as follows:
public static float sin(double angle) {
if (angle >= 0.0) {
return Mth.SIN[(int) ((long) (angle * 10430.378350470453 + 0.5) & 65535L)];
} else {
return -Mth.SIN[(int) ((long) (-angle * 10430.378350470453 + 0.5) & 65535L)];
}
}
public static float cos(double angle) {
if (angle >= 0.0) {
return Mth.SIN[(int) (((long) (angle * 10430.378350470453 + 0.5) + 16384L) & 65535L)];
} else {
return Mth.SIN[(int) (((long) (-angle * 10430.378350470453 + 0.5) + 16384L) & 65535L)];
}
}This runs at essentially the same speed as the current implementation.
Addendum:
The following video contains modded gameplay in version 26.2 to increase the decimal precision of the players facing.
https://www.youtube.com/watch?v=Sm6mxu2a9SQ
The following behaviour is observed:
- If the internal (unwrapped) yaw of the player is positive, the players yaw is rounded counterclockwise to the nearest of the 65536 possible angles.
- If the internal yaw of the player is negative, the players yaw is rounded clockwise to the nearest angle.
Thus all angles are completely shifted by 1 unit for positive rotations compared to negative.
There might still be a small issue in adding the NUM_ANGLES / 4term after casting to long, which is that longs have the capabilitiy to overflow. Considering a player has to do about 150 trillion full rotations to reach that point and overflowing not changing the final result much due to the mod 2^n calculation after, this shouldnt be an issue.
Thank you for helping us improve Minecraft! We saved your files: