Eye Movement with UV Offsets
AUTHOR NOTE: This tutorial uses affiliate links. I only use affiliate links for assets that are either explicitly used in the article or that I have personally tested. I do not include affiliated links for paid promotional purposes.
Last week I published a tutorial about setting up eye movement. That post focused on using bones for the eye transforms.
To recap, eye bones are super easy to set up and ideal if:
If your character has separate eye meshes rigged and weighted to bones, and
The eye meshes are spherical
The Problem
But what if:
If your character has eye meshes that are not rigged to bones, or
The eyes are part of the character mesh, but have their own material, or
The eye meshes are flat or non-spherical
My solution:
Let’s offset the eye texture UV instead! This process offsets the texture rather than rotating a mesh. Based on the target position, it will offset the texture on it’s x and y axis so the eye appears to be looking at a target.
Animator Controller Setup
First, let’s set up our Animator Controller. This part might look familiar because it’s the exact same process we used in the previous tutorial.
It may seem odd that I’m setting up behaviors, but I like to make use of the animator to keep things that move the character in one place. If you’re interested in my reasoning, I went on and on about it in the last tutorial.
So let’s be more concise in this one. Here are the steps for setup:
Creating a new layer in your animator called “EyeMovement” or whatever you like. This layer will use an avatar mask for just the eyes and head bone, so cancel out the entire humanoid form and import your character’s avatar to the Transform field. The layer should override the other layers and have the IK Pass enabled.
Set up states for eye movement. (They don’t need animation clips applied to them)
Animator States
Here are the states you should set up. These will have behavior scripts attached to them so they trigger the style of looking we want.
That said, the first state is an empty default state that I’ve called “DefaultEye”, which will not need an animation behaviour since it’s going to serve as the resting state for our eye movement system. When we return to this state, our character will not be tracking any targets with their eyes.
The next state is for when the character is looking at and tracking a target with their eyes. I’ve called this state “LookAtTarget” and this is where I’ll start setting up the animation behaviour. (The behaviour scripts will be different depending on the method of eye movement you choose.)
Lastly, for testing purposes, go ahead and create two trigger parameters, one called DefaultEye and another called LookAtTarget. We’ll use these triggers as the transition conditions between the two animation states.
What you need:
Any character with eyes that have their own material reference. I’ll be using my sci-fi version of Scarlett Riley.
Our Eye Rig:
Our setup for this offsets is a bit more complex than using eye transforms. We can’t just get our offset calculations directly from a lookat system, because we’re trying to translate the target position to the x and y values of a 2D texture.
So what we’re going to do is create a rig that captures where the eyes would be facing if they were looking at a target, and then use that position to calculate our texture offset. The easiest way to do this is to take advantage of the Transform.LookAt API.
Here’s my thinking: Basically, the eye rig holds a child object. When we ask the rig (parent) transform to “look at” a target, the child’s local position becomes our reference point. However, if we just grab a reference to the child objects local position, then we will always get zero, since it’s a child of the rig, which has rotated, not changed position. Instead, we’ll calculate the child objects local position using the head transform as the parent.
With that in mind, let’s build our eye rig.
Start by creating a child gameobject on the head bone of your character named EyeRig_Holder. This can be a primitive if you want to be able to judge the positioning, just remember to disable or delete the renderer when you’re not doing that. You’ll want this centered between, and a few units ahead of the characters eyes.
Next, let’s create a child of the EyeRig_Holder called ChildPosition. Move this child a few units infront of the holder. (Think of the holder like a cone, where the apex is pointed at the child.) Now duplicate this object and drag/drop it to be parented to the head, rather than the rig. Call this one “DefaultEyePositon“.
The Monobehavior:
Now for our monobehaviour. Create a new script and call it “EyeUVMovement” or whatever you like. Here’s how the code will look:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EyeUVMovement : MonoBehaviour
{
[Header("Target Setup")]
public Transform mainArmature;
public Transform head;
public GameObject eyes;
public Material eyeMat;
public Transform lookTarget;
public Transform defaultTarget;
public float lookWeightMultiplier = 0f;
public Vector2 lookOffset;
[Header("Eye Rig")]
//This is the transform that will "look at" targets
public Transform eyeRig_holder;
//This is the transform that we will get the position from
public Transform childPosition;
//This is a reference to a zero-ed out position so the eye rig can return to it's default rotation
public Transform defaultEyePosition;
void Start()
{
eyeMat = eyes.GetComponent().material;
lookWeightMultiplier = mainArmature.transform.localScale.x;
}
public void SetLookTarget(Transform _target)
{
lookTarget = _target;
}
}
That’s a lot of parameters!
The Eye Rig parameters are self explanatory, since we just built them, but let me break down why the others are needed:
mainArmature: The Armature parent for your character rig. The look weight for the texture offset is going to be dependent on scale. We use this to get the scale needed to calculate an appropriate weight multiplier.
head: The head transform for your character rig. This will be used to calculate the local position of the rig’s child object.
eyes: This references the eye mesh. We’ll use it to get a reference to the…
eyeMat: The material for the eye mesh. This contains the texture that we will be offsetting.
lookTarget: The target we want to look at.
defaultTarget: A permanent default target reference for our eyes zeroed out state. (This is not the same as our DefaultEyePosition child object.)
lookWeightMultiplier: This will help us calculate the offset amount (see mainArmature).
lookOffset: The texture offset values we will be manipulating.
Go back into your project and populate all of the values accordingly. If you run the scene now it won’t do anything because still have to create our behaviour script to manipulate the values.
The State Behaviors:
We’ve already created our DefaultEye and LookAtTarget states. Leave the Default state alone and select the LookAtTarget state. Add a new behaviour script to the state. I’ve called mine “LookUVBehaviour”.
The parameters we need to make the system work properly will include:
a reference to our eye movement system,
a Vector2 to represent the offset value, and
floats for the offset limits. You can get your limits directly by going to the eye mesh material and manipulating the x and y values on the texture to find the ranges you’re happy with.
How it works
In the OnStateEnter function, we’ll grab a reference to our eye movement system. We’ll also use the OnStateUpdate function, where we’ll ask our eye rig to “look at” our target, translate the world position of our child object to the local position of the head transform, and then set our texture offset within the limits we’ve cast.
(We get the local position relative to a different parent with InverseTransformPoint, which takes a world position of a point and converts it to a local position, based on the parent you’ve indicated.)
Lastly, in OnStateExit we will ask the eyerig to look at our default position again and zero out the texture offset.
Here’s the full LookUVBehaviour script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LookBehaviourUV : StateMachineBehaviour
{
//Weight Values
public float eyeMaxX = 0.15f;
public float eyeMinX = -0.15f;
public float eyeMaxY = 0.08f;
public float eyeMinY = -0.05f;
public Vector2 lookOffset;
public EyeUVMovement eyeUVMovement;
// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
eyeUVMovement = animator.GetComponent();
}
// OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
eyeUVMovement.eyeRig_holder.LookAt(eyeUVMovement.lookTarget);
//Set up the local position from the head transform
lookOffset = new Vector2(eyeUVMovement.head.InverseTransformPoint(eyeUVMovement.childPosition.position).x * eyeUVMovement.lookWeightMultiplier, -eyeUVMovement.head.InverseTransformPoint(eyeUVMovement.childPosition.position).y * eyeUVMovement.lookWeightMultiplier);
if (lookOffset.x < eyeMinX || lookOffset.x > eyeMaxX)
{
lookOffset.x = Mathf.Clamp(lookOffset.x, eyeMinX, eyeMaxX);
}
if (lookOffset.y < eyeMinY || lookOffset.y > eyeMaxY)
{
lookOffset.y = Mathf.Clamp(lookOffset.y, eyeMinY, eyeMaxY);
}
eyeUVMovement.eyeMat.SetTextureOffset("_MainTex", lookOffset);
}
// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
eyeUVMovement.eyeRig_holder.LookAt(eyeUVMovement.defaultTarget);
eyeUVMovement.eyeMat.SetTextureOffset("_MainTex", new Vector2(0,0));
}
// OnStateMove is called right after Animator.OnAnimatorMove()
//override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
//{
// // Implement code that processes and affects root motion
//}
// OnStateIK is called right after Animator.OnAnimatorIK()
//override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
//{
// // Implement code that sets up animation IK (inverse kinematics)
//}
}
Now, either set the LookAtTarget state to be default, or trigger the animation state with a menu button to test it out. You can see that the characters eyes will follow the target. If we go back to the default state, they will revert to their original rotation and stop tracking the target. It’s pretty simple.
The drawback here is that we don’t get the subtle head and body movements that are built into the IK system like we do with bone transforms. You could set this up following the tutorial for bone transforms, but only using the head and body weights. I haven’t tried it, but I would expect that it would work.
Happy Look Targeting!
#Unity3D #Character #CodingTutorial #Animation



