Learn more about the development of mixed reality 3D models of dinosaur fossils. Create smart models in tangible and virtual forms infused with artificial intelligence capabilities.
Learn about the Virtual Reality workflow to create 3D models in Unity and the Windows Mixed Reality platform.
Learn about the Tangible Reality workflow to create 3D models with embedded sensors and printable physical objects.
Eric Poitras
Assistant Professor
Department of Educational Psychology
University of Utah
Kirsten Butcher
Associate Professor
Department of Educational Psychology
University of Utah
Matt Orr
Graduate Research Assistant
Department of Educational Psychology
University of Utah
Michelle Hudson
Graduate Research Assistant
Department of Educational Psychology
University of Utah
Support for this work has been provided by the College of Education at the University of Utah, the Windows Mixed Reality Funding Program, and the Natural History Museum of Utah.
The software required to build virtual reality apps are listed below:
Ensure that your PC meets the minimal hardware requirements to ensure proper performance - including graphics card, ports, bluetooth, and CPU/storage. For the complete list of supported devices, visit the Windows Mixed Reality Devices. To learn whether your own device meets the requirements, download the Windows Mixed Reality App Check from the Windows Store.
This documentation only applies to Windows Mixed Reality headsets. Additional requirements for developing for HoloLens 1 or 2 are not listed here.
Get the latest release of the Windows Mixed Reality Toolkit and read about any recent updates.
Export the 3D model as a .obj file. Otherwise, Unity also supports several different file extension types.
Export the texture as a .png file.
Customize the settings of the WMRT to change the background color of the scene boundaries.
Add components to the 3D model to create a bounding box to enable user interactions.
Enables users to drag the 3D model and change its position.
Create a new script.
Add C# functions in the script to log user interaction events. The 'O' key may be used to indicate the start of the experiment, while the 'P' key is used to close the application at the end of an experiment. In order to ensure consistent frame rate per second, choose Edit > Project Settings. In Quality settings, set the VSync Count option to 'Dont Sync'.
using System; using System.IO; using System.Text; using System.Media; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.XR; public class NewBehaviourScript : MonoBehaviour { string path; public override bool Equals(object other) { return base.Equals(other); } public override int GetHashCode() { return base.GetHashCode(); } public override string ToString() { return base.ToString(); } public int frame_count = 0; public int trial_count = 0; public int trial_duration = 11; public int frame_rate = 10; public int trial_condition = 0; public string audio_clawing = Directory.GetCurrentDirectory().ToString() + @"\Assets\Clawing.wav"; public string audio_observing = Directory.GetCurrentDirectory().ToString() + @"\Assets\Observing.wav"; public SoundPlayer player_clawing = new SoundPlayer(); public SoundPlayer player_observing = new SoundPlayer(); // Start is called before the first frame update void Start() { QualitySettings.vSyncCount = 0; Application.targetFrameRate = frame_rate; Time.captureFramerate = frame_rate; string directory = Directory.GetCurrentDirectory().ToString(); string logName = @"\gameLog"+ DateTime.Now.ToString("MMMM dd") + ".txt"; path = directory + logName; Debug.Log(message: GetLogEntry("New_Session_Event")); Debug.Log(message: path); Debug.Log(message: audio_clawing); Debug.Log(message: audio_observing); this.player_clawing.SoundLocation = audio_clawing; this.player_clawing.LoadAsync(); this.player_observing.SoundLocation = audio_observing; this.player_observing.LoadAsync(); } // Update is called once per frame void Update() { if (Input.GetKeyDown(KeyCode.Space)) { Debug.Log(message: GetLogEntry("Space_Key_Down")); }else if(Input.GetKeyUp(KeyCode.Space)) { Debug.Log(message: GetLogEntry("Space_Key_Up")); } else if (Input.GetKeyDown(KeyCode.A)) { Debug.Log(message: GetLogEntry("A_Key_Down")); } else if (Input.GetKeyUp(KeyCode.A)) { Debug.Log(message: GetLogEntry("A_Key_Up")); } else if (Input.GetKeyDown(KeyCode.W)) { Debug.Log(message: GetLogEntry("W_Key_Down")); } else if (Input.GetKeyUp(KeyCode.W)) { Debug.Log(message: GetLogEntry("W_Key_Up")); } else if (Input.GetKeyDown(KeyCode.D)) { Debug.Log(message: GetLogEntry("D_Key_Down")); } else if (Input.GetKeyUp(KeyCode.D)) { Debug.Log(message: GetLogEntry("D_Key_Up")); } else if (Input.GetKeyDown(KeyCode.S)) { Debug.Log(message: GetLogEntry("S_Key_Down")); } else if (Input.GetKeyUp(KeyCode.S)) { Debug.Log(message: GetLogEntry("S_Key_Up")); } else if (Input.GetKeyDown(KeyCode.O)) { Debug.Log(message: GetLogEntry("O_Key_Down_Start_Session")); } else if (Input.GetKeyUp(KeyCode.O)) { Debug.Log(message: GetLogEntry("O_Key_Up_Start_Session")); } else if (Input.GetKeyDown(KeyCode.P)) { Debug.Log(message: GetLogEntry("P_Key_Down_End_Session")); } else if (Input.GetKeyUp(KeyCode.P)) { Debug.Log(message: GetLogEntry("P_Key_Up_End_Session")); Application.Quit(); } Debug.Log(message: GetLogEntry("Frame_Update")); if((frame_count/ frame_rate) >= trial_duration) { trial_count++; frame_count = 0; System.Random random = new System.Random(); trial_condition = random.Next(0, 2); if (trial_condition == 0) { // Control Condition //System.Media.SoundPlayer player = new System.Media.SoundPlayer(audio_observing); //player.Play(); if (this.player_observing.IsLoadCompleted) { this.player_observing.PlaySync(); } } else { // Treatment Condition //System.Media.SoundPlayer player = new System.Media.SoundPlayer(audio_clawing); //player.Play(); if (this.player_clawing.IsLoadCompleted) { this.player_clawing.PlaySync(); } } } else { frame_count++; } } public string GetLogEntry(String value) { //string path = "Assets/Resources/test.txt"; //Write some text to the test.txt file //StreamWriter writer = new StreamWriter(path, true); //writer.WriteLine("'timestamp';'" + DateTime.Now.ToString() + "';'Event_Label';'" + value + "';'Rotation';'Rot_X';'" + transform.rotation.eulerAngles.x.ToString() + "';'Rot_Y';'" + transform.rotation.eulerAngles.y.ToString() + "';'Rot_Z';'" + transform.rotation.eulerAngles.z.ToString() + "';'Scale';'Sca_X';'" + transform.localScale.x.ToString() + "';'Sca_Y';'" + transform.localScale.y.ToString() + "';'Sca_Z';'" + transform.localScale.z.ToString() + "';'Position';'Pos_X';'" + transform.position.x.ToString() + "';'Pos_Y';'" + transform.position.y.ToString() + "';'Pos_Z';'" + transform.position.z.ToString() + "';'Pos_Cam_X';'" + Camera.main.transform.position.x.ToString() + "';'Pos_Cam_Y';'" + Camera.main.transform.position.y.ToString() + "';'Pos_Cam_Z';'" + Camera.main.transform.position.z.ToString() + "';'"); //writer.Close(); Vector3 leftPositionE = InputTracking.GetLocalPosition(XRNode.LeftHand); Quaternion leftRotationQ = InputTracking.GetLocalRotation(XRNode.LeftHand); Vector3 rightPositionE = InputTracking.GetLocalPosition(XRNode.RightHand); Quaternion rightRotationQ = InputTracking.GetLocalRotation(XRNode.RightHand); string input = "'frame';'"+frame_count+"';'trial';'"+trial_count+"';'condition';'"+trial_condition+"';'timestamp';'" + DateTime.Now.ToString() + "';'Event_Label';'" + value + "';'Rotation';'Rot_X';'" + transform.rotation.eulerAngles.x.ToString() + "';'Rot_Y';'" + transform.rotation.eulerAngles.y.ToString() + "';'Rot_Z';'" + transform.rotation.eulerAngles.z.ToString() + "';'Scale';'Sca_X';'" + transform.localScale.x.ToString() + "';'Sca_Y';'" + transform.localScale.y.ToString() + "';'Sca_Z';'" + transform.localScale.z.ToString() + "';'Position';'Pos_X';'" + transform.position.x.ToString() + "';'Pos_Y';'" + transform.position.y.ToString() + "';'Pos_Z';'" + transform.position.z.ToString() + "';'Pos_Cam_X';'" + Camera.main.transform.position.x.ToString() + "';'Pos_Cam_Y';'" + Camera.main.transform.position.y.ToString() + "';'Pos_Cam_Z';'" + Camera.main.transform.position.z.ToString() + "';'Rot_Cam_X';'" + Camera.main.transform.rotation.x.ToString() + "';'Rot_Cam_Y';'" + Camera.main.transform.rotation.y.ToString() + "';'Rot_Cam_Z';'" + Camera.main.transform.rotation.z.ToString() + "';'Sca_Cam_X';'" + Camera.main.transform.localScale.x.ToString() + "';'Sca_Cam_Y';'" + Camera.main.transform.localScale.y.ToString() + "';'Sca_Cam_Z';'" + Camera.main.transform.localScale.z.ToString() + "';'RotationQuat';'Quaternion_W';'" + transform.rotation.w.ToString() + "';'Quaternion_X';'" + transform.rotation.x.ToString() + "';'Quaternion_Y';'" + transform.rotation.y.ToString() + "';'Quaternion_Z';'" + transform.rotation.z.ToString() + "';'Quaternion_Cam_W';'" + Camera.main.transform.rotation.w.ToString() + "';'Quaternion_Cam_X';'" + Camera.main.transform.rotation.x.ToString() + "';'Quaternion_Cam_Y';'" + Camera.main.transform.rotation.y.ToString() + "';'Quaternion_Cam_Z';'" + Camera.main.transform.rotation.z.ToString() + "';'Controllers';'Control_Left_Pos_X';'" + leftPositionE.x.ToString() + "';'Control_Left_Pos_Y';'" + leftPositionE.y.ToString() + "';'Control_Left_Pos_Z';'" + leftPositionE.z.ToString() + "';'Control_Right_Pos_X';'" + rightPositionE.x.ToString() + "';'Control_Right_Pos_Y';'" + rightPositionE.y.ToString() + "';'Control_Right_Pos_Z';'" + rightPositionE.z.ToString() + "';'Control_Left_Ori_Euler_X';'" + leftRotationQ.eulerAngles.x.ToString() + "';'Control_Left_Ori_Euler_Y';'" + leftRotationQ.eulerAngles.y.ToString() + "';'Control_Left_Ori_Euler_Z';'" + leftRotationQ.eulerAngles.z.ToString() + "';'Control_Right_Ori_Euler_X';'" + rightRotationQ.eulerAngles.x.ToString() + "';'Control_Right_Ori_Euler_Y';'" + rightRotationQ.eulerAngles.y.ToString() + "';'Control_Right_Ori_Euler_Z';'" + rightRotationQ.eulerAngles.z.ToString() + "';'Control_Left_Ori_Quart_W';'" + leftRotationQ.w.ToString() + "';'Control_Left_Ori_Quart_X';'" + leftRotationQ.x.ToString() + "';'Control_Left_Ori_Quart_Y';'" + leftRotationQ.y.ToString() + "';'Control_Left_Ori_Quart_Z';'" + leftRotationQ.z.ToString() + "';'Control_Right_Ori_Quart_W';'" + rightRotationQ.w.ToString() + "';'Control_Right_Ori_Quart_X';'" + rightRotationQ.x.ToString() + "';'Control_Right_Ori_Quart_Y';'" + rightRotationQ.y.ToString() + "';'Control_Right_Ori_Quart_Z';'" + rightRotationQ.z.ToString() + "';'" ; if (!File.Exists(path)) { using (StreamWriter sw = File.CreateText(path)) { sw.WriteLine(input); return input; } } else { using (StreamWriter sw = File.AppendText(path)) { sw.WriteLine(input); return input; } } } public void LogOrientationEventStart() => Debug.Log(message: GetLogEntry("Rotation_Start_Event")); public void LogOrientationEventStop() => Debug.Log(message: GetLogEntry("Rotation_Stop_Event")); public void LogScaleEventStart() => Debug.Log(message: GetLogEntry("Scale_Start_Event")); public void LogScaleEventStop() => Debug.Log(message: GetLogEntry("Scale_Stop_Event")); public void LogPositionEventStart() => Debug.Log(message: GetLogEntry("Position_Start_Event")); public void LogPositionEventStop() => Debug.Log(message: GetLogEntry("Position_Stop_Event")); }
The functions will log user interactions and properties related to the camera and object in a text file labelled by the date (i.e., Month and Day) in the main directory of your Unity project. This feature is currently available in the debugger version opened directly from the Unitor editor window. You will see the log entries in the Console panel as well as the preview in the Game panel.
Assign script functions to handle events.
The hardware required to build tangible models with sensors are listed below:
Setup the monitor by following the steps shown below:
Install the Raspbian operating system by following these steps:
Enable your I2C Communication
Make the wiring (See the following pins)
Download the folder structure of the "MPU9250" folder of the GitHub project to the Desktop of the Raspberry Pi. Create a new folder in the directory called "logfile.txt" (Link).
In the Terminal, navigate to the directory by typing cd Desktop. Then, choose cd MPU9250.
Type "sudo pip install FaBo9Axis_MPU9250". You may need to install pip by typing "sudo apt-get install python-pip".
Type "python program.py" to run the program and register the sensor input values to the debugger console. You can open the program by navigating to the directory in the Terminal and typing "python program.py".
Use the following script in the program.py file to log events with a timestamp and enable logging of button press events.
import FaBo9Axis_MPU9250 import time import datetime import sys from gpiozero import Button button = Button(4) def start_calibration(): st = str(datetime.datetime.now())+";" print(st+";Start Calibration") file.write(st+";Start Calibration") file.write("\n") def end_calibration(): st = str(datetime.datetime.now())+";" print(st+";End Calibration") file.write(st+";End Calibration") file.write("\n") button.when_pressed = start_calibration button.when_released = end_calibration #import keyboard mpu9250 = FaBo9Axis_MPU9250.MPU9250() file = open("logfile.txt","a") try: while True: accel = mpu9250.readAccel() print " ax = " , ( accel['x'] ) ax = " ax = ;"+str(accel['x'])+";" print " ay = " , ( accel['y'] ) ay = " ay = ;"+str(accel['y'])+";" #file.write(" ay = " ( accel['y'] )) print " az = " , ( accel['z'] ) az = " az = ;"+str(accel['z'])+";" #file.write(" az = " ( accel['z'] )) gyro = mpu9250.readGyro() print " gx = " , ( gyro['x'] ) gx = " gx = ;"+str(gyro['x'])+";" #file.write(" gx = " ( gyro['x'] )) print " gy = " , ( gyro['y'] ) gy = " gy = ;"+str(gyro['y'])+";" #file.write(" gy = " ( gyro['y'] )) print " gz = " , ( gyro['z'] ) gz = " gz = ;"+str(gyro['z'])+";" #file.write(" gz = " ( gyro['z'] )) mag = mpu9250.readMagnet() print " mx = " , ( mag['x'] ) mx = " mx = ;"+str(mag['x'])+";" #file.write(" mx = " ( mag['x'] )) print " my = " , ( mag['y'] ) my = " my = ;"+str(mag['y'])+";" #file.write(" my = " ( mag['y'] )) print " mz = " , ( mag['z'] ) mz = " mz = ;"+str(mag['z'])+";" #file.write(" mz = " ( mag['z'] )) print st = str(datetime.datetime.now())+";" file.write(st+ ax + ay + az + gx + gy + gz + mx + my + mz) file.write("\n") time.sleep(0.1) except KeyboardInterrupt: file.close() sys.exit()
In the terminal, navigate to the Desktop, then MPU9250 using the "cd" directive. Type "python program.py". Use the Ctrl+C keys to stop the program. The calibration period is initiated by pressing the button, and then releasing it to indicate the end of calibration and beginning of the study.