Eye contact is an important factor to make characters feel alive and aware of yours and other things' presence. In this chapter, we'll make a control that will follow a spatial with its eyes, as shown in the following screenshot:
Eye tracking can be implemented in a single control using the following steps:
EyeTrackingControl
that extends AbstractControl
.Bone
fields: one called leftEye
and another called rightEye
. Furthermore, we should add a spatial called lookAtObject
and a related Vector3f
called focusPoint
.setSpatial
method, we find and store the bones for leftEye
and rightEye
.lookAtObject
.controlUpdate
method. First of all, we need to take control of the bones or we won't be able to modify their rotation, as shown in the following code:if(enabled && lookAtObject != null){ leftEye.setUserControl(true); rightEye.setUserControl(true);
lookAtObject
position that is relative to the eyes. We do this by converting the position to model space and storing it in focusPoint
, as shown in the following code:focusPoint.set(lookAtObject.getWorldTranslation().subtract(getSpatial().getWorldTranslation()));
Vector3f
gives us the relative direction:Vector3f eyePos = leftEye.getModelSpacePosition(); Vector3f direction = eyePos.subtract(focusPoint).negateLocal();
direction
vector. We can apply this on our eyes after modifying it a bit as its 0-rotation is up:Quaternion q = new Quaternion(); q.lookAt(direction, Vector3f.UNIT_Y); q.addLocal(offsetQuat); q.normalizeLocal();
setUserTransformsWorld
. Finally, we give the control of the bones back to the system using the following code:leftEye.setUserTransformsWorld(leftEye.getModelSpacePosition(), q); rightEye.setUserTransformsWorld(rightEye.getModelSpacePosition(), q); leftEye.setUserControl(false); rightEye.setUserControl(false);
The actual code is a fairly straightforward trigonometry, but knowing what values to use and the flow of doing it can be tricky.
Once the class receives an object to look at, it subtracts the model's worldTranslation
from lookAtObjects
so they end up in a coordinate system that is relative to the model's origo point also called modelspace.
Using setUserTransformsWorld
also sets the position, but since we supply its current modelSpacePosition
, no change will be applied.
Actually, the direction of each eye should be calculated separately for the result to be entirely correct.
By now, the character has a very intent stare at the camera. This is an improvement, but it can be made more lifelike. Something that may not be so obvious is that we rarely look at the same point all the time even if we look at the same object. We can emulate this behavior by adding a random bit of flickering to the control:
private float flickerTime = 0f; private float flickerAmount = 0.2f; private Vector3f flickerDirection = new Vector3f();
By introducing these three fields, we have a base for what we want to do:
flickerTime += tpf * FastMath.nextRandomFloat(); if(flickerTime > 0.5f){ flickerTime = 0; flickerDirection.set(FastMath.nextRandomFloat() * flickerAmount, FastMath.nextRandomFloat() * flickerAmount, 0); } direction.addLocal(flickerDirection);
This piece of code goes in the middle of the controlUpdate
method, right after calculating the direction. What we do is we increase flickerTime
until it reaches 0.5f (note that this is not in seconds since we apply a random number). Once this happens, we randomize flickerDirection
based on flickerAmount
and reset flickerTime
.
With each consecutive update, we will apply this to the calculated direction and slightly offset the focus point.
18.221.254.61