Experimenting with Tracking & Depth Perception in VR
This experiment was meant to highlight the difference between rotation and position tracking as an element of VR experiences, as well as investigate how depth perception relies on both forms of tracking. The written code includes scripts to mirror a user’s head movement (position and orientation), disable positional tracking, disable rotational tracking and reposition stimuli of various sizes to appear as similar sizes at the same depth. Although these tasks may seem trivial, the process of disabling rotational tracking in Unity using OVR can be tricky. Hopefully the included code helps a few people out there struggling to find a solution given the limitations of the OVR assets.
VR Mirror
This simple script creates a Virtual Reality Mirror of sorts. It considers the tracked position and orientation of the head mounted display and then manipulates a game object’s rotation and position to match or mirror the pose. The difference between mirroring and matching is whether the object is facing the camera. If the object is matching movements, then if you bring your face closer to the screen, the object moves further away from the screen. If the object is mirroring movements, then bringing your face closer to the screen will move the object closer to the screen as well.
using UnityEngine;
using System.Collections;
public class VRMirror : MonoBehaviour
{
public CameraFlipper cameraFlipper;
public GameObject copyCat;
private Vector3 copyCatStartingPosition;
private Transform eyeCenter;
// Use this for initialization
void Start()
{
eyeCenter = transform.Find("TrackingSpace/CenterEyeAnchor");
copyCatStartingPosition = copyCat.transform.position;
}
// Update is called once per frame
void Update()
{
if (cameraFlipper.getShouldMirror())
{
/**
* Make the cube mirror your movements (positional and
* rotational). This means that the cube is facing the camera (and as a result, the user should
* see the cube’s face). In this case, if you bring your face close to the screen, the cube moves
* closer to the screen as well. Imagine looking into a mirror to get an intuition of this.
*/
//NOTE:
//This implementation has the copy cat mirror the player's movement but does this mirroring in its own frame (relative to its starting position)
//The offset of the player's movement is equal to the position of the eyeCenter relative to the OVRCameraRig
//Similarly, the copycat logs its starting position for reference later, as its relative origin for mirrored movement.
//Doing things this way enables versaitility later, in case one wishes to have many copycat objects in different locations, mirroring the same target.
OVRPose mirroredPose = eyeCenter.ToOVRPose();
mirroredPose.orientation *= Quaternion.Euler(0.0f, 180.0f, 0.0f);
mirroredPose.orientation.w *= -1.0f;
mirroredPose.orientation.z *= -1.0f;
mirroredPose.position.z *= -1;
mirroredPose.position += copyCatStartingPosition;
copyCat.transform.FromOVRPose(mirroredPose);
}
else
{
/**
* Make the cube match your movements. This includes
* positional and rotational movements . Furthermore, note that this also means that the cube
* should be looking in the same direction the camera is (and as result, the user should see the
* back of the cube’s head). For example, if you bring your face closer to the screen, the cube
* moves further away from the screen.
*/
OVRPose newPose = eyeCenter.ToOVRPose();
newPose.position = newPose.position + copyCatStartingPosition;
copyCat.transform.FromOVRPose(newPose);
}
}
}
Disabling Position and Rotation Tracking
The following script provides a means of disabling or rather toggling the position and or rotation tracking of the head mounted display. Being able to selectively toggle the two forms of tracking helps demonstrate how much they contribute to the VR experience.
Although disabling tracking is generally a bad idea, there do exist reasons for a developer to want the build in tracking disabled. Perhaps a developer wants to implement their own form of tracking, or simply prefers to have the executive choice to do so. Ideally the development tools available would have simple toggles for these forms of tracking that would cut the tracking process off at the base to save performance. Unfortunately no such toggle exists for rotational tracking when using the OVR assets for the Unity Game Engine. I had to get a little creative with the solution. What I wound up with was a glorified “hack” that counteracted/cancelled the rotation after it was applied. If anyone has a better solution let me know!
Direct transformations cannot be applied the center eye anchors within the tracking space. I’m not sure why, but I assume it has to do with late updates or order of execution internally. Instead the solution involved rotating the parent space (tracking space) such that the updated rotation was effectively cancelled.
using UnityEngine;
using System.Collections;
public class ToggleTracking : MonoBehaviour
{
private Transform eyeCenter;
private Transform trackingSpace;
private bool bUseRotationalTracking = true;
private bool bUsePositionalTracking = true;
private Vector3 lastKnownHMDPosition;
private Quaternion lockedHMD_GlobalOrientation; //used for checking accuracy
private Quaternion lastKnownHMD_GlobalOrientation;
void Start()
{
eyeCenter = transform.Find("TrackingSpace/CenterEyeAnchor");
trackingSpace = transform.Find("TrackingSpace");
}
// Update is called once per frame
void Update()
{
checkForInput();
}
void LateUpdate()
{
if (!bUsePositionalTracking)
{
counteractTranslation();
}
if (!bUseRotationalTracking)
{
counteractRotation();
}
}
void checkForInput()
{
//Pressing the R key should toggle rotation tracking on and off.
if (Input.GetKeyDown("r"))
{
bUseRotationalTracking = !bUseRotationalTracking;
print("Toggling rotation tracking to " + bUseRotationalTracking);
if (!bUseRotationalTracking)
{
lastKnownHMD_GlobalOrientation = eyeCenter.transform.rotation;
lockedHMD_GlobalOrientation = eyeCenter.transform.rotation;
}
}
//Pressing the P key should toggle position tracking on and off.
if (Input.GetKeyDown("p"))
{
bUsePositionalTracking = !bUsePositionalTracking;
print("Toggling positional tracking to " + bUsePositionalTracking);
if (!bUsePositionalTracking)
{
lastKnownHMDPosition = eyeCenter.transform.position; // ToOVRPose().position;
}
}
}
//counteracts any positional changes
void counteractTranslation()
{
Vector3 currentHMDPosition = eyeCenter.transform.position;
Vector3 deltaPosition = currentHMDPosition - lastKnownHMDPosition;
trackingSpace.position -= deltaPosition;
}
//counteracts any additional tracked rotation of the headset by rotating its parent tracking space to negate the tracked rotation
void counteractRotation()
{
Quaternion HMD_deltaRotation = lastKnownHMD_GlobalOrientation * Quaternion.Inverse(eyeCenter.transform.rotation);
trackingSpace.rotation = HMD_deltaRotation * trackingSpace.rotation;
lastKnownHMD_GlobalOrientation = eyeCenter.rotation;
//checkAccuracy();
}
//Checks the accuracy of the counteracted rotation...
//Cmpares the current global orientation of the Center Eye Anchor to the global orientation at the time of disabling tracking
void checkAccuracy()
{
Vector3 diff = lockedHMD_GlobalOrientation.eulerAngles - eyeCenter.rotation.eulerAngles;
float thresh = 0.01f;
if (Mathf.Abs(diff.x) > thresh || Mathf.Abs(diff.y) > thresh || Mathf.Abs(diff.z) > thresh)
{
print("" + diff.x + ", " + diff.y + ", " + diff.z);
}
}
}
Depth Perception and Relative Size
This script rearranges game objects so that they appear to be of equal size when viewed. Additionally a small sequence demonstrates the effect by randomly assigning an order to the objects and displaying them one by one.
using UnityEngine;
using System.Collections;
public class GenerateStimuli : MonoBehaviour
{
public GameObject[] stimulusObjs; //specify the objects in the inspector for use in the stimulus loop
public GameObject targetEye; //the reference point that will be used to determine how stimuli should be rearranged
private bool bSeqInProgress = false; //true for the duration of the displayed stimulus sequence
public float desiredAngularDiameterInDeg; //will determine the apparent size of all the stimuli
public float spacingAngleDegrees; //will determine the apparent spacing of the stimuli
// Update is called once per frame
void Update()
{
checkForInput();
}
void checkForInput()
{
//Pressing the S key should activate the main sequence, only if any previously activated sequences are finished
if (Input.GetKeyDown("s"))
{
print("received request to run main sequence");
if (!bSeqInProgress)
{
//run the main sequence
mainSequence();
}
else
{
print("Main sequence already running");
}
}
}
//The main sequence I'll be using as an example
void mainSequence()
{
print("initiating main sequence");
bSeqInProgress = true;
setAllStimuliActive(stimulusObjs, false);
randomizeStimOrder(stimulusObjs);
Vector3[] positionAssignments = determinePositions(stimulusObjs, targetEye, desiredAngularDiameterInDeg, spacingAngleDegrees);
assignPositions(stimulusObjs, positionAssignments);
StartCoroutine(showStimuli(stimulusObjs));
}
//Activates all stimulus in a target array
void setAllStimuliActive(GameObject[] objs, bool bStimActive)
{
foreach (GameObject stimulusObj in objs)
{
stimulusObj.SetActive(bStimActive);
}
}
//Randomize the order of stimuli in an array of stimulus objects
void randomizeStimOrder(GameObject[] objs)
{
ShuffleArray(objs);
}
//converts an angle from degrees to radians
float degToRad(float deg)
{
return deg * Mathf.PI / 180.0f;
}
//returns the distance an object with "actualDiameter" must be from the view point to yield the desired angular diameter
float distFromAngDiam(float angularDiameter, float actualDiameter, bool bAngDiamProvidedInRadians)
{
float distance;
if (bAngDiamProvidedInRadians)
{
distance = 0.5f * actualDiameter / Mathf.Sin(0.5f * angularDiameter);
}
else
{
distance = 0.5f * actualDiameter / Mathf.Sin(0.5f * degToRad(angularDiameter));
}
return distance;
}
//determines the positions of stimulus objects in array such that they appear to be the same diameter and appear spaced apart by the same angle
Vector3[] determinePositions(GameObject[] objs, GameObject eye, float angularDiameter, float spacingAngle)
{
Vector3[] determinedPositions = new Vector3[objs.Length];
float fixedHeight = eye.transform.position.y;
for (int i = 0; i < objs.Length; i++)
{
Vector3 calculatedPos = new Vector3();
calculatedPos.y = fixedHeight; //the y position of each stimulus center is fixed at the eye height
//retrieve the actual diameter of the stimulus sphere
float actualDiameter = 2 * objs[i].GetComponent<SphereCollider>().radius * objs[i].transform.lossyScale.x;
//determine the distance from the eye at which the diameter appears to be the desired angular diameter
float distance = distFromAngDiam(angularDiameter, actualDiameter, false);
//determine the x and z offsets (opposite and adjacent sides of right triangle) that are necessary to produce that distance
//this is derived from the right triangle, solving for an opposite side length given an angle and a hypotenuse
float xOffsetUnitForGivenDist = distance * Mathf.Sin(degToRad(spacingAngle));
//the x offset will be a multiple of the desied spacing... as the center object will be offset by 0, and the left by +x and right by -x
float xOffset = (Mathf.Floor(objs.Length / 2) - i) * xOffsetUnitForGivenDist;
calculatedPos.x = eye.transform.position.x + xOffset;
//the z offset will be the adjacent side of a right triangle now that we know the hypotenuse and the opposite side length
float zOffset = Mathf.Sqrt(Mathf.Pow(distance, 2) + Mathf.Pow(xOffset, 2)); //triangle side length
calculatedPos.z = eye.transform.position.z - zOffset;
determinedPositions[i] = calculatedPos;
}
return determinedPositions;
}
//assigns pre-determined positions to an array of objects
void assignPositions(GameObject[] objs, Vector3[] assignments)
{
for (int i = 0; i < objs.Length; i++)
{
objs[i].transform.position = assignments[i];
}
}
//shows all the stimulus objects from an array of stimulus objects
IEnumerator showStimuli(GameObject[] objs)
{
//for each stimulus going left -> right, wait 3 seconds between each display
foreach (GameObject obj in objs)
{
yield return new WaitForSeconds(3.0f);
obj.SetActive(true);
}
bSeqInProgress = false;
print("main sequence complete");
}
//shuffles an array, useful for randomizing
public static void ShuffleArray<T>(T[] array)
{
for (int i = array.Length - 1; i > 0; i--)
{
int randIndex = Random.Range(0, i);
T tmp = array[i];
array[i] = array[randIndex];
array[randIndex] = tmp;
}
}
}
Comments