Snapping Quaternion Rotations in Script

Hi. I have a bit of a problem concerning quaternions. In the game we’re making, the player can position and rotate objects on all axes which is all done in script. But we want these positions and rotations to be snapped to certain increments (0.1 for movement and 5 for rotation). The movement is fine as can just do the good old:

Vector3 position = SelectedObject.position;
position.x = Mathf.Round(position.x / MovementIncrement) * MovementIncrement;
position.y = Mathf.Round(position.y / MovementIncrement) * MovementIncrement;
position.z = Mathf.Round(position.z / MovementIncrement) * MovementIncrement;
SelectedObject.position = position;

But the rotations are being a bit more tricky to snap. I’m currently rotating the objects via:

SelectedObject.Rotate(RotateIncrement * direction);

Where direction is a vector (Vector.up for example) that describes the axis to rotate around. This does kinda work but after running the game, playing with the rotations and then trying to put the object back to (0,0,0) the rotations are actually something like (1.567, 0.2345, -1.234) which are of course no longer multiples of five. I thought all I’d need to do is the same as the position snapping:

Vector3 rotation = SelectedObject.eulerAngles;
rotation.x = Mathf.Round(rotation.x / RotateIncrement) * RotateIncrement;
rotation.y = Mathf.Round(rotation.y / RotateIncrement) * RotateIncrement;
rotation.z = Mathf.Round(rotation.z / RotateIncrement) * RotateIncrement;
SelectedObject.eulerAngles = rotation;

But this makes the rotations look very weird as they snap when they shouldn’t due to the way Quaternions work and makes it look very jittery.

I tried using AngleAxis but this created the same problem as using just the Rotate function. I also tried storing and manipulating good old Eular vectors and then converting to quaternion after doing vector rotations but then this caused gimbal lock to happen.

Is there any way to snap quaternion rotations to multiples of 5 on all axes in script?

I’m not sure of what you need in terms of rotations, but try this script. Start with a new scene, add a cube, and add this script. While running, use the Arrow keys and the A/D keys for rotation.

using UnityEngine;
using System.Collections;

public class Example : MonoBehaviour
{
	private Vector3 vCurr = Vector3.zero;

	void Start() {
		transform.eulerAngles = vCurr;
	}

	void Update() {
		Vector3 vNext = vCurr;

		if (Input.GetKeyDown (KeyCode.UpArrow)) {
			vNext.x += 5.0f;
		}

		if (Input.GetKeyDown (KeyCode.DownArrow)) {
			vNext.x -= 5.0f;
		}

		if (Input.GetKeyDown (KeyCode.LeftArrow)) {
			vNext.y += 5.0f;
		}
		if (Input.GetKeyDown (KeyCode.RightArrow)) {
			vNext.y -= 5.0f;
		}
		if (Input.GetKeyDown (KeyCode.A)) {
			vNext.z += 5.0f;
		}
		if (Input.GetKeyDown (KeyCode.D)) {
			vNext.z -= 5.0f;
		}

		if (vNext != vCurr) {
			transform.eulerAngles = vNext;
			vCurr = vNext;
		}
	}
}

Note that I never read Transform.eulerAngles. The rotation is always taken from my vCurr Vector3.

If I recall Quaternion math correctly, I believe multiplying them together is effectively what we would observe as adding them. Maybe try something like…

transform.rotation *= Quaternion.Euler(new Vector3(RotateIncrement,0,0));

Since this way we’re assigning the rotation as Quaternions instead of from Euler values, it might work better?

I could be totally off on this. It’s been a while since I’ve dealt with rotations. I could also be completely misunderstanding your problem. I apologize in advance if either of these are the case! :slight_smile:

This should work hopefully, this is a slightly edited version of my answer here.

Just check the Update method on how to call my RotateAround function and then attach this to your object you want to rotate:

private var targetAngle : Quaternion;
private var startAngle : Quaternion;
 
private var forward : Vector3;
private var right : Vector3;
private var up : Vector3;

var increment : float = 5;
var speed : float = 1.0;
 
private var startTime : float;
 
function RoundAngle(v : Quaternion) : Quaternion{
	var vE : Vector3 = v.eulerAngles;
	vE.x = Mathf.Round(vE.x/increment)*increment;
	vE.y = Mathf.Round(vE.y/increment)*increment;
	vE.z = Mathf.Round(vE.z/increment)*increment;
	v = Quaternion.Euler(vE);
	return v;
}

function RotateAround(vF : Vector3, vR : Vector3, vU : Vector3, axis: Vector3, angle: float) : Quaternion{
	var rot: Quaternion = Quaternion.AngleAxis(angle, axis); // get the desired rotation
	forward = (rot*vF);
	right = (rot*vR);
	up = (rot*vU);

	startTime = Time.time;
	startAngle = transform.rotation;
	targetAngle = RoundAngle(rot * targetAngle);
}

function Start(){
	startAngle = transform.rotation;
	targetAngle = startAngle;
	forward = (transform.forward);
	right = (transform.right);
	up = (transform.up);
}

function Update () {
	if(Input.GetKeyDown(KeyCode.RightArrow)){
		RotateAround(forward, right, up, Vector3.up, -increment);
	}else if(Input.GetKeyDown(KeyCode.LeftArrow)){
		RotateAround(forward, right, up, Vector3.up, increment);
	}
	if(Input.GetKeyDown(KeyCode.UpArrow)){
		RotateAround(forward, right, up, Vector3.right, increment);
	}else if(Input.GetKeyDown(KeyCode.DownArrow)){
		RotateAround(forward, right, up, Vector3.right, -increment);
	}
	transform.rotation = Quaternion.Slerp(startAngle, targetAngle, (Time.time-startTime)/speed);
}

Hope that does what you want, it seems to work for me, you can change ‘increment’ to whatever angle (in degrees) you want to snap to, and you can change speed to snap to the angle faster (measured in seconds, currently it will take 1 second to snap).

Scribe