Community
cancel
Showing results for 
Search instead for 
Did you mean: 
MartyG
Honored Contributor III
288 Views

Unity Tip: Limiting The Rotation of TrackingAction-Powered Spherical Joints

Hi everyone,

If you have developed a project in Unity that uses perfectly spherical objects (such as Sphere type Unity objects) to rotate an object whose position is fixed on the spot, then you may have found that the Virtual World Box settings are no help when it comes to limiting how far that object can rotate in a particular direction before it is stopped.  

This is because a perfect sphere whose position is static can only rotate round and round where it is standing and never touch the sides of a Virtual World Box surrounding the object.  Because the Box cannot obstruct the rotation, its movement can never be limited, except when the camera loses tracking of the hand or face landmark that is driving the motion.

Not being able to limit rotation is particularly problematic if you are developing a complex construction such as a full-body avatar with fully working limbs.  This is because the joints allow limbs that are attached to them to rotate past the point that human physiology normally allows, increasing the chances that the joint will become unstable as stress builds up in it and "flip", throwing the limb out of the position that you want it to be in.

After a year and a half of experiments however, I have finally scripted a simple limiting mechanism that sets precise limits on how many angles a joint may rotate before it is stopped from traveling any further in that axis direction and gently moved back away from the limit point automatically.  Below, I have provided a step by guide technique to constructing this mechanism in your own Unity projects.

********

STEP ONE

Create a C# format script file inside the Unity rotation object that contains your rotation-creating Action SDK script (e.g TrackingAction).  Do not worry if you are not familiar with C# - the complete script code will be provided, and each part of the script explained as we go.  

The primary reason for using C# is that it is faster than JavaScript.  This is partly because C# is a faster language in general, and partly because Unity always processes C# scripts first and JavaScripts second.  Also, Unity is moving towards a focus primarily on C# in the future, so by using C# now you are future-proofing the code for re-use in other projects at a later date.

In my own project, I created a C# script called RotationCorrection_RightShoulder

1.jpg

The script was given a name that refers to a specific limb part of my avatar because each avatar part will need its own limiter script that is tailored to how far it is allowed to rotate before it is stopped.  For example, a lower arm would be able to travel a much greater number of angles as it bends than the upper arm, whose movement range is more restricted.

STEP TWO

Open the script file that you have just created in your Unity script editor by double left clicking on the script's icon in your 'Assets' panel or by left-clicking on the small gear-wheel icon at the end of the script's name in the 'Inspector' panel (see above) and selecting the 'Edit Script' option from the menu that pops up.

When the new script file has opened in your script editor, you will see that Unity has placed a default block of code in the script right from the start.

2.jpg

It is at this point that beginners in C# scripting will experience the first of the differences between JavaScript and C#.  A C# script always uses the name that you gave the script file at the top of it.  You must never change this, as your script will not run if this "public class" name does not match up exactly to the name that you gave your script.

Lower down, you will see two blocks of code - a "void Start()' block, and a 'void Update()' block.  If you have already used JavaScript then you will be familiar with the concept of defining blocks of tasks using Functions called Start and Update - for example, 'function Start()' and 'function Update()'.  The only difference here is that the word 'function' is replaced by the word 'void'.  

Apart from that, it works the same way as functions in JavaScript.  A 'Start()' function will run the script just one time and then stop, whilst an 'Update' function will cause your script to loop infinitely until something happens to stop it running.

BTW, the two lines at the top of the script - using UnityEngine' and 'using System Collections' - are simply a mechanism to enable your script to "boot up" by telling Unity where it can find the necessary resources to make the script run.  You can ignore these lines, as you will never have to change them.

STEP THREE

Now we are ready to start writing our rotation limiter script.

We want our rotation to have a minimum and maximum number of rotation degrees that it can travel through, and we never want the object to be able to rotate outside of the degree values in this range.  

One of the problems that we have is that we need to define a very narrow range of degrees in which the limiter will be activated, otherwise the limiter will be active almost constantly and make effective rotation of the object impossible.  For example, if we want the maximum rotation to be 100 degrees, then we should state that the limiter will be activated if the rotation angle of the object is between 100 and 110 degrees.  Outside of this narrow range, the limiter should not be activated by the rotation of the object by the user's hand or face movements.

You may wonder why we do not make the activation range even narrower - such as between 100 and 101 degrees.  The reason for this is that the degrees change very fast when being changed by an Action script.  We therefore need to give our limiter script enough time to detect when the object's rotation has moved into the limiter range.  If the trigger range was just 1 degree then the object's rotation would have moved beyond the desired maximum range before the limiter script could react and stop it from occurring.

A second important reason for defining a narrow window for activation of the limiter is that we are going to be using an infinitely-looping Update() type function in our script.  If we did not do this - if we used a Start() function, for example - then the script would only check the rotation angles once when it first boots up and then never check again, rendering the limiter mechanism useless.  What we want is for the script to be checking the rotating object's angles repeatedly so that it can catch when the rotation degrees reach a point where the object needs to be stopped from going any further in a particular direction.

Let's get started.  In your default C# script, position the cursor below the 'public class' name-defining line and use the Enter key to create a block of empty lines below it.

using UnityEngine;
using System.Collections;

public class RotationCorrection_RightShoulder : MonoBehaviour {







	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
	
	}
}

Paste the following lines into the empty block that you have just created:

private float rotationanglex1;
private float rotationanglex2;
private float rotationangley1;
private float rotationangley2;

Your script should now look like this:

using UnityEngine;
using System.Collections;

public class RotationCorrection_RightShoulder : MonoBehaviour {

	private float rotationanglex1;
	private float rotationanglex2;
	private float rotationangley1;
	private float rotationangley2;

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
	
	}
}

Here is another difference between JavaScript and C#.  We create "variables" as containers to store certain values or text information within so that we can call on the information in that variable later on simply by referring to its name.  This saves us from having to type out the same sequence of numbers or letters over and over.

In JavaScript code, a variable will typically be called 'Var'.  Whilst C# can use Var to define variables too, it is more common for them to be given names that describe the type of information that they are storing.  For instance, 'float' (as seen above) is suited particularly to numbers that have a decimal point in them.  'Int' - short for Integer - handles round numbers like '3', and 'string' contains information based on alphabetic letters rather than numbers.

The prefix 'private' or 'public' determines whether the information contain in the variable will be limited to the script file that they are contained in (private) or whether the information can be accessed by other scripts (public).  If you are writing a program that uses more than one script that talk to each other, using 'public' variables ensures that you will avoid getting an error about the variables being "protected" from being accessed.

In our case though, our script will not be communicating with another script outside of it, and so using a 'private' type variable is fine.

Let us now explain the names of our four variables - rotationanglex1, rotationanglex2, rotationangley1 and rotationangley2.  If you look carefully at the names, the format is 'rotation angle' - informing us at a glance what the variable's purpose is - followed by X1, X2, Y1 and Y2.

The full-body avatar in my project uses a TrackingAction that is constrained to travel in the X and Y direction axes, with Z locked to provide stability to movement.  X1 and X2 will be used to define the minimum and maximum range of travel of the X axis, whilst Y1 and Y1 will be used to define the minimum and maximum travel of the Y axis.

STEP FOUR

We want our script to loop infinitely to continuously check the rotation values of our object.  We therefore edit the block of code to delete the 'Start()' block of code and leave only the 'Update()' section remaining.

3.jpg

using UnityEngine;
using System.Collections;

public class RotationCorrection_RightShoulder : MonoBehaviour {

	private float rotationanglex1;
	private float rotationanglex2;
	private float rotationangley1;
	private float rotationangley2;

void Update () {
	
	}
}

STEP FIVE

Next, we need to tell the script which information it is supposed to be storing inside the four variables that we have defined.  We want the script to read the current rotation angle of an object's axes and store them inside the correct variables so that their degree value can be checked to see whether it falls within the range where rotation further beyond that minimum or maximum range should be stopped.

The rotation value of a specific object can be tracked using the 'Rotation' row of the values in the Transform section at the very top of the 'Inspector' panel of Unity.  The values constantly update rapidly as the object moves and rotates.  It is also in the Transform area where we can create changes that cause changes to the movement of an object from outside of the Action SDK script that is driving the object's motion.

4.jpg

The position and rotation values of an object can be checked at any given instant with scripting and their contents stored inside a variable.  Just as the Transform section's values are constantly updating, the value stored in the variable will be updated simultaneously as it changes in real-time.

To check the rotation of the X, Y and Z axes, we can use the following instructions:

transform.eulerAngles.x

transform.eulerAngles.y

transform.eulerAngles.z

In our script, we want to check the min and max of the X axis and the Y axis.  We therefore tell the script that the four variables should store the values of the X and Y axes.

using UnityEngine;
using System.Collections;

public class RotationCorrection_RightShoulder : MonoBehaviour {

	private float rotationanglex1;
	private float rotationanglex2;
	private float rotationangley1;
	private float rotationangley2;

void Update () {

		rotationanglex1 = transform.eulerAngles.x;
		rotationanglex2 = transform.eulerAngles.x;

		rotationangley1 = transform.eulerAngles.y;
		rotationangley2 = transform.eulerAngles.y;
	
	}
}

STEP SIX

With our variables now set up, we need to create a way to tell the script how to check the rotation values stored inside the variables and decide whether they fall within the range where the limiter mechanism should be activated.  We will do this using Logic Statements.

Logic Statements use terms such as If, And, Or to set up the rules for how information should be checked, and what the script should do if certain conditions are found to have been met.  For example, 'IF' this has happened, 'AND' this has also happened, then do this.

The rules that are set up can also use math rules such as '==' (equals), '<' (less than' and '>' (greater than) to help the script to make decisions about what action to take - whether to activate an event or do nothing and continue checking until it decides that the conditions have been met, then activate the appropriate event.

We insert into the 'Update()' section of our script the following 'If' type instruction:

	if (rotationanglex1 > 320 && rotationanglex1 < 330) 
{

			Debug.Log("Hello X1");

		}
		}

What the logic in this instruction is saying is:

A.  IF the variable rotationanglex1 is found to contain a rotation value for the object that is Greater Than 320 (degrees) and Less Than 320 (degrees);

B. THEN print a message in the Debug Console section at the base of the Unity window (accessed by clicking on the 'Console' option tab on the 'Assets' panel) that tells us that something happened - i.e the object's rotation reached the maximum point of between 320 and 330 degrees (a 10 degree wide checking window) where we want the script to stop the object from rotating any further onward.

5.jpg

Our script now looks like this:

using UnityEngine;
using System.Collections;

public class RotationCorrection_RightShoulder : MonoBehaviour {

	private float rotationanglex1;
	private float rotationanglex2;
	private float rotationangley1;
	private float rotationangley2;

void Update () {

		rotationanglex1 = transform.eulerAngles.x;
		rotationanglex2 = transform.eulerAngles.x;

		rotationangley1 = transform.eulerAngles.y;
		rotationangley2 = transform.eulerAngles.y;

		if (rotationanglex1 > 320 && rotationanglex1 < 330) {
			Debug.Log("Hello X1");

		}
	
	}
}

STEP SEVEN

At present though, our script is not of much practical use, because we are only being told when the rotation limit point has been reached - the script is not actually stopping the rotation from proceeding past that point.  We need to program in our limiter mechanism!

How we will achieve this is to add an instruction to the script that tells it to make a change to the rotation values in the Transform section of the Inspector panel that automatically rotates the object 10 degrees backwards away from the limit point.  It effectively pushes the object outside of the rotation value range that causes the limiter to be activated, so that when the script checks the rotation value again a second later, it will find that the angle of the object is no longer inside the 'danger range' and will take no further action until the next time that rotation value is back inside the limit range.

It is vital that we give the object enough of a backward push to take it out of the range where the script takes action against the object.  Because we are using an infinitely looping 'Update()' type script, if the object remained inside the trigger range then every time the script checked the rotation values, it would activate the repulsion event again and again, causing the object - and anything attached to it, such as our avatar arm - to be constantly rotated backward over a period of time until the script decided that it should take no further action.  We want the script to gently nudge the object away slightly when the rotation limit is reached, not shove it hard!

As an example of the effect that we want to avoid: in early versions of this script, our avatar's arm was force-pushed halfway across the body so that it swung from hanging at the side of the torso to doing a proud hand salute across the chest!  By minimizing the amount of time that the repulsion effect is active by moving the object's angle out of the trigger range in the very first milliseconds of activation, we achieve the slight, barely visible repulsion away from the limit point that we are seeking.

Another way to illustrate the repulsion effect using popular media is in a very early episode of season one of 'Star Trek: The Next Generation'.  The crew have been made drunk and incapable by a virus, the engine controls have been messed up and a big chunk of space rock is heading for the Enterprise.  Young Wesley Crusher overcomes his inebriation enough to rapidly reprogram the starship Enterprise's object-attracting tractor beam into a repulsor beam that pushes against the abandoned starship Tsiolskovsky nearby (pronounced Zil-kov-ski) and so shoves the Enterprise backward away from it, giving Data the android enough extra seconds to fix the engine controls and warp the ship away from the rock before it collides.

We are going to use a type of rotation called a Slerp for our counter-rotation.  How a Slerp differs from other types of rotation you may have used is that instead of instantly jumping an object's position from one point to another, it smoothly rotates the object from one point to another, so that the resulting rotation effect looks much more pleasant.

We add the following line to our script below the Debug instruction:

		transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler (10, transform.eulerAngles.y, transform.eulerAngles.z),
				Time.deltaTime * 1.5f);

It looks more complicated than it actually is.  You will remember from earlier in this guide that the rotation value of an object can be read using the instruction 'transform.eulerAngles', with the x, y or z axis following after it.

What this means is that if you tell the object to change to its transform.EulerAngles value then you are effectively telling it to keep the rotation of a particular directional axis exactly the same as it currently is.  Being able to do this is vital to using Slerps for your rotation.  This is because a Slerp insists on being given three values for the rotation - X, Y and Z - even if you want to only change one of the axes.  By assigning a transform.eulerAngles value to the axes that you do not want to change, you ensure that those axes remain correct, whilst only the axis that you actually want to affect is altered.

In the line of code above, we are therefore telling the script to rotate the X axis of the object 10 degrees backward away from the zone in which the limiter effect is activated, whilst keeping the Y and Z axes of the object unchanged.  The 'Time.deltaTime' instruction at the end of the line dictates how much distance an object has traveled since the last time Unity checked (in the previous processing "frame").  Measuring distance traveled within a certain period of time (the length of a Unity processing frame) is a way of expressing how fast the object is moving.

The larger the value that you use, the faster the rotation will occur, but the less smooth that the visual appearance of that movement will be.  This is why the TrackingAction script uses a 'smoothing' feature to help prevent rapid movements from looking jerky.

If you give Time.deltaTime a decimal value instead of a round number, always remember to put an 'f' (for Float) after the number so that Unity knows to treat the value as a decimal, otherwise you will get a red error and the script will not run.  This is another difference between JavaScript and C# - JavaScript is far less fussy about decimals than C# is!

So now our script should look like this:

using UnityEngine;
using System.Collections;

public class RotationCorrection_RightShoulder : MonoBehaviour {

	private float rotationanglex1;
	private float rotationanglex2;
	private float rotationangley1;
	private float rotationangley2;

void Update () {

		rotationanglex1 = transform.eulerAngles.x;
		rotationanglex2 = transform.eulerAngles.x;

		rotationangley1 = transform.eulerAngles.y;
		rotationangley2 = transform.eulerAngles.y;

		if (rotationanglex1 > 320 && rotationanglex1 < 330) {
			Debug.Log("Hello X1");
			transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler (10, transform.eulerAngles.y, transform.eulerAngles.z),
				Time.deltaTime * 1.5f);
		}
	
	}
}

STEP EIGHT

Do a test run of your project!  If the script is working correctly then whenever you move your object to an angle between 100 and 110 degrees, the automatic repulsion should activate and slightly rotate the object back towards the 0 direction.  This movement should be accompanied by a message in the Debug Console stating with a "Hello X1' message that the event has been triggered.

Once you are satisfied that the limiter mechanism is functioning correctly, you can remove the 'Debug.Log' line from the script to stop the message from being generated in the console.

STEP NINE

Once we had got the limiter mechanism working for the maximum value of the X axis of our avatar's shoulder joint sphere, we were able to add a further three 'If' statements to the script to define the rules for checking the minimum rotation of the X axis, and the min-max values for the Y axis.  This is what the finished script looks like.

using UnityEngine;
using System.Collections;

public class RotationCorrection_RightShoulder : MonoBehaviour {

	private float rotationanglex1;
	private float rotationanglex2;
	private float rotationangley1;
	private float rotationangley2;

	void Update () {

		rotationanglex1 = transform.eulerAngles.x;
		rotationanglex2 = transform.eulerAngles.x;
		rotationangley1 = transform.eulerAngles.y;
		rotationangley2 = transform.eulerAngles.y;

		// OR statement - || symbol
		// If you use AND - && - then both conditions must be satisfied
		// and not just one of them.
		// For a 'between range A and range B' instruction though, use &&


		if (rotationanglex1 > 320 && rotationanglex1 < 330) {
			Debug.Log("Hello X1");
			transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler (10, transform.eulerAngles.y, transform.eulerAngles.z),
				Time.deltaTime * 1.5f);
		}


		if (rotationanglex2 > 100 && rotationanglex2 < 110) {
			Debug.Log("Hello X2");
			transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler (-10, transform.eulerAngles.y, transform.eulerAngles.z),
				Time.deltaTime * 1.5f);
		}


		if (rotationangley1 > 330 && rotationangley1 < 340) {
			Debug.Log("Hello Y2");
			transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler (transform.eulerAngles.x, 10, transform.eulerAngles.z),
				Time.deltaTime * 1.5f);
		} 

		if (rotationangley2 > 30 && rotationangley2 < 40) {
			Debug.Log("Hello Y2");
			transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler (transform.eulerAngles.x, -10, transform.eulerAngles.z),
				Time.deltaTime * 1.5f);
		} 

	}
}

When adding new blocks of code, your biggest problem may be getting the number of { and } (opening and closing) brackets correct.  C# is just as fussy about these as it is with decimal points, and if there are too few or too many brackets then you may get a red-error message about a 'Parsing Error' in the debug console.

When the brackets are incorrect, Unity will flag this up by placing a red underline beneath the bracket nearest to where the error is occurring.  If it is not obvious why the error is occurring, a good tip is to just add or subtract brackets around the highlighted one, one at a time, until the red underline disappears, indicating that the error has been corrected.

6.jpg

STEP TEN - THE FINALE

As mentioned at the start of the guide, you will need to create a separate version of this script for each of the individual objects that you want to place a limiter mechanism on.  In our avatar, for example, we created a variant of the script for the lower arm bending joint once we had prototyped the script in the shoulder joint first, and tailored the limiter range values specifically for that joint. 

If you have any questions about the methods used in this guide, please feel free to ask in the comments below.  Good luck!

0 Kudos
2 Replies
MartyG
Honored Contributor III
288 Views

As well as auto-rotating an object smoothly from point A to point B, you can also instantly rotate the object to its end location by using a non-Slerp quaternion rotation instruction in place of the Slerp one in the guide above.  Example:

transform.rotation = Quaternion.Euler(5, transform.eulerAngles.y, transform.eulerAngles.z);

The above line instantly sets the object's X axis rotation to a value of '5' when the conditions of the If statement are met.

As we said at the start of the guide, an instant snap-to-location is not desirable if you are trying to achieve a smooth-looking reset.  This instant-reset method though is particularly useful for dealing with objects whose positions change at the moment that the face or hand is detected, as once the object goes out of the desired position during this initial start-up snap then the above line of rotation code can reset the object to the start-position degrees that you want it to have.

I applied the technique practically to the eyelids in my full-body avatar.  The lids rise and lower in response to the real-life lid movements.  However, when the face is detected the lids were jumping into the half-closed position instead of remaining wide open until I consciously wanted to lower them.  

Once I used the above line of code to program the lids with a reset mechanism that was triggered once the lid-closing reached a certain range of degrees, the lids snapped back to their fully-open default position on start-up of face detection.  In other words, the undesired leaping of the lids could not be stopped, but the visual impact of this to the user was minimized by auto-correcting the lids as soon as the issue occurred.

MartyG
Honored Contributor III
288 Views

Another interesting thing that I found in my testing is that even if a TrackingAction axis is constrained, it can still sometimes move slightly away from its starting angle when the unconstrained axes are moving.  Your only chance for the locked axis to get back to its normal frozen angle therefore is if you are lucky enough for the movements of the unconstrained axes to take it in that direction instead of moving further away from the frozen axis' start point.

In my avatar's shoulder joint, for example, it began at a 5 degree deviation from the Z axis' constrained angle of 0, being 355 degrees instead of 0.  Over time, the shoulder joint was being rotated further and further from its proper alignment.  Because Z influenced how far the avatar arm was to the side of the body (i.e the armpit open and close), the arm was very slowly lifted more and more sideward away from the body.

To correct this problem, I set up a C# script file in the shoulder joint, based on the instant-rotation quaternion code in the comment above, to tell the Z angle of the joint to continually try to rotate back to 0 from wherever it currently was.  Since the joint typically began with a 5 degree deviation in my own project, I set the code to rotate Z to '-5' degrees.

void Update () {

		transform.rotation = Quaternion.Euler(transform.eulerAngles.x, transform.eulerAngles.y, -5);

	}
}

Now when the project was run, the Z angle would hover around the 0 to 0.5 degree point constantly no matter where the X and Y axes of the shoulder were moved to.  Because the script was forcing the Z axis back to 0 degrees in every frame of the script's looping Update() function, it could never travel far from its constraint-frozen start point.

Whether or not to use this technique in your project is a matter of personal choice, and auto-correcting a drifting constrained joint is not likely to make much difference to the control of a simple object.  If you are controlling a complex multi-part object such as an avatar arm that interacts regularly with other nearby objects though, having greater control over your object's angles and keeping them within the limits that you have defined helps to keep them stable and avoid them "flipping out" if those limits are exceeded by the object's rotation angle.

Reply