- Home /
Rotation snapping overshooting on certain rotations.
Hi, I've been trying to get a function working to snap rotation of a block on each axis to increments of 90 degrees. The problem I've been having is that, no matter how I seem to approach applying rotation, I get slight miscalculations at certain rotations.
The code:
void RotateBlock()
{
if (buildMode == BuildMode.Active)
if (!IsACube())
{
float xAngle = 0;
float yAngle = 0;
float zAngle = 0;
if (Input.GetKeyDown(KeyCode.G))
xAngle = 90f;
if (Input.GetKeyDown(KeyCode.R))
yAngle = 90f;
if (Input.GetKeyDown(KeyCode.F))
zAngle = 90f;
if (mirrorMode == MirrorMode.Inactive)
{
tempBlock1.transform.Rotate(
Mathf.RoundToInt(xAngle),
Mathf.RoundToInt(yAngle),
Mathf.RoundToInt(zAngle),
Space.World);
}
else
{
tempBlock1.transform.Rotate(
Mathf.RoundToInt(xAngle),
Mathf.RoundToInt(yAngle),
Mathf.RoundToInt(zAngle),
Space.World);
tempBlock2.transform.Rotate(
Mathf.RoundToInt(xAngle),
Mathf.RoundToInt(-yAngle),
Mathf.RoundToInt(-zAngle),
Space.World);
}
}
else return;
}
On -90 and +90 rotations for each axis, I'm getting a deviation of -/+ 0.00001 which is causing me problems elsewhere because I need to be able to check an object's rotation against a vector. I'm also getting a weird assignment (-1.525879e-05) to the Y and Z axes every other time the X rotation snaps to -180.
I'm sure I'm just doing something stupid, but I'm at a loss as to what. Any help would be hugely appreciated. For the record, I've tried each of the angles being an int, rounding the rotational assignment with Mathf.Round, Mathf.RoundToInt, or without any rounding at all.
Another odd thing I noticed, when these blocks are reinstantiated from the save file and the rotation is assigned from an explicitly declared Quaternion.Euler, the same behaviours seem to be happening.
Answer by unity_ek98vnTRplGj8Q · Mar 02, 2020 at 04:40 PM
Ah yes floating point approximation strikes yet again... TLDR: positions and rotations in Unity cannot be reliably set and read due to the fact that floating point numbers cannot exactly express many fractions. In reality you are just going to have to deal with it, but usually there is a pretty simply workaround depending on your application. If you give me more information on why you need to check the rotation angles or why you need to check them against a vector I can help out with that, but generally doing things like if (rotation.eulerAngles.y == 90)
is bad practice.
The reason these errors happen is because floating point numbers use base 2, and not all simple base 10 numbers can be expressed simply in base 2. Just like how 1/3 cannot be expressed in a finite number of decimal places in base 10, similar things happen in base 2 when trying to express some simple fractions. The solution is just to go out to as many decimal places as will fit into a float and dropping anything after that. This results in values that are extremely close to the desired value but not exact. This is often only really an issue with fractions, as generally you don't get large enough integers to break down floating point precision. But even when rotating by small integers, like 90 degrees, you will still get issues as Unity converts these angles into a Quaternion, and the internal values in a quaternion are usually small fractions resulting in some floating point error. There are two common solutions to this kind of issue:
1. You can keep track of the "desired" rotation yourself, instead of reading it from the objects rotation directly. That way you know your "desired" rotation is exactly 90 degrees, while you actual rotation is very very very close to 90 degrees.
2. You can allow for a small degree of error. Instead of checking if the rotation is exactly 90 degrees, you can check if the rotation is 90 degrees +- 0.001 degrees
Thinking about it, I actually have the rotations stored as an enum for use in the array, I really should have just been checking against that the whole time.
For some reason my previous reply was deleted, which is frustrating.
I'll try to be more brief this time. I have turrets that can be placed in various orientations that snap to said 90° increments. The turrets track a ray hit point and rotate their bodies around the local Y axis and their barrels on the barrels local X axis.
I was trying to get the orientation of the turret so I could restrict rotation on the relevant axes.
I essentially did exactly what you said was bad practice.
Thanks for the explanation, you've been a huge help :)
Ah, this makes a lot of sense. Thanks for the explanation.
What I have are turrets that can be placed by the user (in any 90° snapped orientation) that will track a ray from the camera. The turrets rotate a main body around their local Y axis and pitch the main part of the weapon around the local X axis (I'm actually still trying to get the main rotation working before I even touch the pitch on the X axis).
I was trying to find a quick and dirty way (I'm very new to this, I accept a lot of my code is probably doing things in the wrong way) to check the orientation of the turret so that I know which axis to ignore when rotating towards the ray point.
I hope I haven't missed anything relevant here. Let me know if so.
This is the code, apologies in advance, I can see I've done exactly what you said was bad practice!
private void RotateBase(int index)
{
Vector3 aimPoint = hitInfo.point;
Vector3 targetDirection = aimPoint - weaponPlatform[index].transform.position;
if (weaponPlatform[index].transform.up == Vector3.up || weaponPlatform[index].transform.up == Vector3.down) targetDirection.y = 0.0f;
else if (weaponPlatform[index].transform.up == Vector3.left || weaponPlatform[index].transform.up == Vector3.right) targetDirection.x = 0.0f;
else if (weaponPlatform[index].transform.up == Vector3.forward || weaponPlatform[index].transform.up == Vector3.back) targetDirection.z = 0.0f;
Quaternion rotationGoal = Quaternion.LookRotation(targetDirection);
Vector3 temp = weaponPlatform[index].transform.rotation.eulerAngles;
temp.y = rotationGoal.eulerAngles.y;
rotationGoal.eulerAngles = temp;
weaponInfo.text = $"Target position: {aimPoint} \n" +
$"Weapon position {weaponPlatform[index].transform.position} \n" +
$"Target direction: {targetDirection} \n" +
$"Current rotation: {weaponPlatform[index].transform.rotation.eulerAngles} \n" +
$"Desired rotation: {rotationGoal.eulerAngles}";
Quaternion newRotation = Quaternion.RotateTowards(weaponPlatform[index].transform.rotation, rotationGoal, turnRate);
weaponPlatform[index].transform.rotation = newRotation;
}
Ok this actually isn't too bad of a way to do this, but yea you will run into problems with the vector comparisons. I think the cleanest way to do this is storing the orientation as an enum, especially if you already have this enum available like you mentioned. If you don't want to do this, then using dot product will clean up those if statements a little bit
if($$anonymous$$athf.Abs(Vector3.Dot(weaponPlatform[index].transform.up, Vector3.up)) > 0.99f) targetDirection.y = 0;
else if($$anonymous$$athf.Abs(Vector3.Dot(weaponPlatform[index].transform.up, Vector3.right)) > 0.99f) targetDirection.x = 0;
else targetDirection.z = 0;
Also if you are getting weird rotations then I think change Quaternion.LookRotation(targetDirection) to Quaternion.LookRotation(targetDirection, weaponPlatform[index].transform.up)
Oh and the 3 lines where you are messing with the eulerAngles seem like they could cause some issues, I'm not exactly sure what you are trying to do there but I can almost guarantee that this is not the correct way to do it.