Realtime rotation snapping to 45 degrees

Hi! Im working on a project where the player can pickup objects and rotate them. When holding a button, the object is then snapped to the nearest 45 degree solution.

I’ve tried back and forth to get this to work, and i thought the way i did this would work, but it snaps to one angle only, no matter how i rotate the object. Im struggling to get my head around the problem at the moment, so figured i could ask here.

The snapping happens inside a macro that is currently running on a tick, for testing. The macro gets its inputs from the object world rotation. And is outputting the objects new rotation. I might be doing the math wrong, or i might have missed something, could you help me figure out why this doesn’t work, or if you know of a better solution to get this object to snap, i would be grateful!

The macro:

Inside the macro, showing the X+ and X-. The macro has the X+ and X- repeated for Y and Z below.

Yeah, it would, when the player presses the button, start snapping with modulo.

Hi, thanks for replying! I dont think your method would work in my case. The object in question is being rotated manually in 360 degrees by the player using the mouse. Like in an inspection system where the object is in front of the player, if the player moves the mouse, it rotates accordingly, if a button is pressed, the rotation should snap to 45 degrees to whichever 45 degrees around that 360 free rotation it is closest to. This was the best video i found to explain how i want the rotation done

- YouTube Here the objects you hold snap to 45 degrees in all angles and when you move the mouse it snaps to the next one.

I do not see a single reason why it would not be doable using the aforementioned method. Have you tried implementing it? Start snapping with modulo when the player presses the modifier button.

I did try to implement it, but i haven’t used modulo before so i might be implementing it wrong.

I tried implementing it outside of the macro, getting the Z directly and checking any snapping like that, that didn’t work either, as it never goes past the branch checking if the modulo output is 0

Im most likely doing something wrong, because i dont see this

Thank you so much!

The Snap to grid (float) did it for me. I wasn’t aware that existed.

I’ll have a look at interpolation to see if i can add that to it! :slight_smile:

Awesome, good luck!

This function works for all possible rotations, for all angles <= 90degree. You can also code it completely in Blueprint if you like.

FRotator GridSnap_Rotation(const FRotator& rotator, const float gridDeg)
{
    if (FMath::IsNearlyZero(gridDeg)) return rotator;
	// We assume grid size values of max 90

	const float gridRad = FMath::DegreesToRadians(gridDeg);
	FQuat quat = rotator.Quaternion();

	FVector forward = quat.GetForwardVector();
	FVector right = quat.GetRightVector();

	// Forward
	// To get the forward vector, we snap the forward vector of the quaternion
	// to a linear representation of the rotation.
	FVector forwardResult;
	{
		// Bring the vector into linear space
		forwardResult = FVector(
			FMath::Asin(forward.X)
			, FMath::Asin(forward.Y)
			, FMath::Asin(forward.Z)
		);

		// Snap in linear space
		forwardResult = FVector(
			FMath::GridSnap(forwardResult.X, gridRad)
			, FMath::GridSnap(forwardResult.Y, gridRad)
			, FMath::GridSnap(forwardResult.Z, gridRad)
		);

		// Back to sine space
		forwardResult = FVector(
			FMath::Sin(forwardResult.X)
			, FMath::Sin(forwardResult.Y)
			, FMath::Sin(forwardResult.Z)
		);

		// Ensure our forward vector is not Zero
		if(forwardResult.IsNearlyZero())
		{
		   TArray<float> maxHelper = { FMath::Abs(forward.X), FMath::Abs(forward.Y), FMath::Abs(forward.Z) };
		   int32 maxIndex = INDEX_NONE;
		   FMath::Max(maxHelper, &maxIndex);
		   forwardResult[maxIndex] = FMath::Sign(forward[maxIndex]);
		}

		// Snapping each component of the vector to the grid does not yet place the vector
		// in the rotation grid (when z != 0). We need to make a correction that also normalizes
		// the vector again.
		// E.g 45deg rotated around the y axis and then 45deg rotated around the z axis.
		// Cause of the component snapping, all components of the vector are now 0.707 (sin space).
		// Only the z component is valid to have 0.707, X and Y must be adjusted.
		// The proper result must be FVector(0.5, 0.5, 0.707)
		float sizeXYTarget = FMath::Sqrt(1 - FMath::Square(forwardResult.Z));
		FVector2D vec2D = FVector2D(forwardResult);
		float size2d = vec2D.Size();
		if (FMath::IsNearlyZero(size2d))
		{
			vec2D *= 0;
		}
		else
		{
			vec2D *= sizeXYTarget / size2d;
		}

		forwardResult.Normalize();
	}

	// Right
	// To get the right vector we rotate in grid distance around the forward vector
	// and take the vector that is closest to the original right vector.
	FVector rightResult;
	{
		FVector rightTemp;
		if (forwardResult.Equals(FVector(0.f, 0.f, 1.f)))
		{
			rightTemp = FVector(0.f, 1.f, 0.f);
		}
		else if (forwardResult.Equals(FVector(0.f, 0.f, -1.f)))
		{
			rightTemp = FVector(0.f, -1.f, 0.f);
		}
		else
		{
			rightTemp = FVector(0.f, 0.f, 1.f) ^ forwardResult;
			rightTemp.Normalize();
		}
		FVector bestMatch = rightTemp;
		float distClosest = FVector::DistSquared(rightTemp, right);

		bool bInversed = false;
		bool bWasCloser = false;
		int32 rotMultiplier = 0;
		while (true)
		{
			rotMultiplier = rotMultiplier + (bInversed ? -1 : 1);
			FVector rightRotated = rightTemp.RotateAngleAxis(gridDeg * rotMultiplier, forwardResult);
			float dist = FVector::DistSquared(rightRotated, right);
			if (dist < distClosest || FMath::IsNearlyEqual(dist, distClosest, KINDA_SMALL_NUMBER))
			{
				bWasCloser = true;
				distClosest = dist;
				bestMatch = rightRotated;
			}
			else if (dist > distClosest)
			{
				// Getting further away from our target
				if (!bInversed)
				{
					// First time, inverse rotation
					bInversed = true;
				}
				else if(bWasCloser)
				{
					// Have been closest possible already and getting further away again: closest possible found
					break;
				}
			}
		}

		rightResult = bestMatch;
	}

	// Up
	FVector upResult;
	{
		upResult = forwardResult ^ rightResult;
		upResult.Normalize();
	}

	FRotator out = UKismetMathLibrary::MakeRotationFromAxes(forwardResult, rightResult, upResult);
	ensure(!out.ContainsNaN());
	return out;
}