- VR starter tutorial part 1
- Introduction
- Getting everything ready
- Viewport nodes for displaying UI on MeshInstance nodes, see the Using a Viewport as a texture tutorial. It covers how to use a Viewport as a render texture, along with how to apply that texture onto a MeshInstance node.
- Starting VR
Physics->Commontab, the physics FPS has been set to90. This makes the physics engine run at the same frame rate as the VR display, which makes physics reactions look smoother when in VR.- Creating the controllers
rumbleto a value, this code will automatically make it fade over time.- This section of code will get the position the player is aiming at with the teleportation raycast and update the teleportation mesh, giving a visual update on where the user will be teleporting to when the release the teleport button.
_physics_process_update_controller_velocityfunction will calculate the VR controller’s velocity through changes in position. It is not perfect, but this process gets a rough idea of the velocity of the VR controller, which is fine for the purposes of this tutorial.- This section of code will keep the held object in the same position and rotation as the VR controller, keeping it synced with the VR controller.
controller_velocityvariable to zero Vector3.controller_velocity. Once the code has gone through all of the cached velocities inprior_controller_velocities, we dividecontroller_velocityby the size of theprior_controller_velocitiesarray, which will give us the combined velocity value. This helps take the previous velocities into account, making the direction of the controller’s velocity more accurate.prior_controller_velocitieshas more than30velocities cached by checking if thesize()function returns a value greater than30. If there are more than30cached velocities stored inprior_controller_velocities, then we simply remove the oldest cached velocity by calling theremovefunction and passing in a index position of0.lengthfunction onmovement_rightormovement_forwardis less than or equal to0, then we simply set thedirectional_movementvariable tofalseso the code knows this VR controller is not moving the player.teleport_mesh.visibleis equal tofalse, then we can teleport with this VR controller. We set theteleport_button_downvariable totrue, setteleport_mesh.visibleto true, and setteleport_raycast.visibletotrue. This will tell the code in_physics_processthat this VR controller is going to teleport, it will make theteleport_meshvisible so the user knows where the are teleporting to, and will maketeleport_raycastvisible to the player has a ‘laser sight’ they can use to aim the teleportation pos.rigid_body, which we’ll be using to store the RigidBody that the VR controller is going to pick up, assuming there is a RigidBody to pick up.grab_areaand grabs the first RigidBody node that does not have a variable/constant namedNO_PICKUPand assigns it to therigid_bodyvariable so we can do some additional post processing later in this function.grab_raycastRaycast node out and checks if it collided with a RigidBody node that does not have a variable/constant namedNO_PICKUP. If it collided with a RigidBody withoutNO_PICKUP, it assigns the node to therigid_bodyvariable so we can do some additional post processing later in this function._throw_rigidbodyfunction step-by-step explanation_on_button_pressed_menufunction step-by-step explanationbutton_releasedfunction step-by-step explanation_on_button_released_triggerfunction step-by-step explanationsleep_area_enteredfunction step-by-step explanationsleep_area_exitedfunction step-by-step explanation
- RigidBody nodes that leave the
Sleep_Areato sleep again, saving performance. - Creating a base class for interactable VR objects
- Let’s quickly go through what this script.
picked_upanddroppedfunctions are stub functions that will be called when the object is picked up and dropped by the VR controller.- Reducing motion sickness
controller_oneandcontroller_two. These variables will hold references to the left and right VR controllers.controller_oneandcontroller_tworespectively. Finally, the vignette shader is made invisible by default by setting it’svisibleproperty tofalse.directional_movementis equal totrueincontroller_oneorcontroller_two. If either of the VR controllers are moving the player, then the vignette shader makes itself visible by setting it’svisibleproperty totrue. If neither VR controller is moving the player, sodirectional_movementisfalsein both VR controllers, than the vignette shader makes itself invisible by setting it’svisibleproperty tofalse.- Final notes
VR starter tutorial part 1
Introduction
This tutorial will show you how to make a beginner VR game project in Godot. one of the most important things when making VR content is getting the scale of your assets correct! It can take lots of practice and iterations to get this right, but there are a few things you can do to make it easier:
- In VR, 1 unit is typically considered 1 meter. If you design your assets around that standard, you can save yourself a lot of headache.
- In your 3D modeling program, see if there is a way to measure and use real world distances. In Blender, you can use the MeasureIt add-on; in Maya, you can use the Measure Tool.
- Google Blocks, and then refine in another 3D modelling program.
- Test often, as the assets can look dramatically different in VR than on a flat screen! Throughout the course of this tutorial, we will cover:
- How to tell Godot to run in VR.
- How to make a teleportation locomotion system that uses the VR controllers.
- How to make a artificial movement locomotion system that uses the VR controllers.
- RigidBody-based system that allows for picking up, dropping, and throwing RigidBody nodes using the VR controllers.
- How to create simple destroyable target.
- RigidBody-based objects that can destroy the targets. Tip Your first 2D game, if you are new to Godot and/or game development. Some experience with making 3D games is required before going through this tutorial series. This tutorial assumes you have experience with the Godot editor, GDScript, and basic 3D game development. A OpenVR-ready headset and two OpenVR-ready controllers are required. This tutorial was written and tested using a Windows Mixed Reality headset and controllers. This project has also been tested on the HTC Vive. Code adjustments may be required for other VR Headsets, such as the Oculus Rift. OpenVR GitHub repository. The starter assets for this tutorial can be found in the releases section on the GitHub repository. The starter assets contain some 3D models, sounds, scripts, and scenes that are configured for this tutorial. Note Credits for the assets provided:
- CGTuts.
- The font used is Titillium-Regular
- -
- License PDF)
- -
- Bastiaan Olij and is released under the MIT license. It can be found both on the Godot Asset Library and on GitHub. 3rd party code and libraries used in the OpenVR addon may be under a different license.
- TwistedTwigleg and is released under the MIT license.
Tip
OpenVR GitHub repository.
Getting everything ready
OpenVR GitHub repository and download the “Starter Assets” file from the releases. Once you have the starter assets downloaded, open up the project in Godot. Note The starter assets are not required to use the scripts provided in this tutorial. The starter assets include several premade scenes and scripts that will be used throughout the tutorial. MeshInstance nodes.
MeshInstance nodes already have scripts attached to them. These scripts will set the texture of a Viewport node to the albedo texture of the material of the MeshInstance node. This is used to display text within the VR project. Feel free to take a look at the script, GUI.gd, if you want. We will not be going over how to to use Viewport nodes for displaying UI on MeshInstance nodes in this tutorial .
Viewport nodes for displaying UI on MeshInstance nodes, see the Using a Viewport as a texture tutorial. It covers how to use a Viewport as a render texture, along with how to apply that texture onto a MeshInstance node.
Before we jump into the tutorial, let’s take a moment to talk about how the nodes used for VR work.
ARVROrigin node is the center point of the VR tracking system. The position of the ARVROrigin is the position the VR system considers the ‘center’ point on the floor. The ARVROrigin has a world scale property that effects the size of the user within the VR scene. For this tutorial, it is set to 1.4, as the world was originally just a tad to big. As mentioned earlier, keeping the scale relatively consistent is important in VR.
ARVRCamera is the player’s headset and view into the scene. The ARVRCamera is offset on the Y axis by the VR user’s height, which will be important later when we add teleportation locomotoin. If the VR system supports room tracking, then the ARVRCamera will move as the player moves. This means that the ARVRCamera is not guaranteed to be in the same position as the ARVROrigin node.
ARVRController node represents a VR controller. The ARVRController will follow the position and rotation of the VR controller relative to the ARVROrigin node. All of the input for the VR controllers happens through the ARVRController node. An ARVRController node with an ID of 1 represents the left VR controller, while an ARVRController controller with an ID of 2 represents the right VR controller.
To summarize:
- ARVROrigin node is the center of the VR tracking system and is positioned on the floor.
- ARVRCamera is the player’s VR headset and view into the scene.
- ARVRCamera node is offset on the Y axis by the user’s height.
- ARVRCamera node may be offset on the X and Z axes as the player moves.
- ARVRController nodes represent the VR controllers and handle all of the input from the VR controllers.
Starting VR
Game.tscn, select theGamenode and make a new script calledGame.gd. In theGame.gdfile, add the following code: GDScript C#extends Spatialfunc _ready(): var VR = ARVRServer.find_interface("OpenVR") if VR and VR.initialize(): get_viewport().arvr = true OS.vsync_enabled = false Engine.target_fps = 90 # Also, the physics FPS in the project settings is also 90 FPS. This makes the physics # run at the same frame rate as the display, which makes things look smoother in VR!
Let’s go over what this code does.using Godot;using System;public class Game : Spatial{ public override void _Ready() { var vr = ARVRServer.FindInterface("OpenVR"); if (vr != null && vr.Initialize()) { GetViewport().Arvr = true; OS.VsyncEnabled = false; Engine.TargetFps = 90; // Also, the physics FPS in the project settings is also 90 FPS. This makes the physics // run at the same frame rate as the display, which makes things look smoother in VR! } }}
_ready function, we first get the OpenVR VR interface using the find_interface function in the ARVRServer and assign it to a variable called VR. If the ARVRServer finds an interface with the name OpenVR, it will return it, otherwise it will return null.
Note
Asset Library or GitHub.
if VR) and another calls the initialize function, which returns a boolean based on whether the OpenVR interface was able to initialize or not. If both of these conditionals return true, then we can turn the main Godot Viewport into an ARVR viewport.
Viewport and set the arvr property to true. This will tell Godot to use the initialized ARVR interface to drive the Viewport display.
90 frames per second, which is the standard for most VR headsets. Without disabling VSync, the normal computer monitor may limit the frame rate of the VR headset to the frame rate of the computer monitor.
Note
Physics->Common tab, the physics FPS has been set to 90. This makes the physics engine run at the same frame rate as the VR display, which makes physics reactions look smoother when in VR.
That is all we need to do for Godot to launch OpenVR within the project! Go ahead and give it a try if you want. Assuming everything works, you will be able to look around the world. If you have a VR headset with room tracking, then you will be able to move around the scene within the limits of the room tracking.
Creating the controllers
RigidBody-based nodes.
Left_Controller.tscn or Right_Controller.tscn. Let’s briefly go over how the scene is setup.
How the VR controller scene is setup
Left_Controller scene has the Controller Id property set to 1 while the Right_Controller has the Controller Id property set to 2.
Note
ARVRServer attempts to use these two IDs for the left and right VR controllers. For VR systems that support more than 2 controllers/tracked-objects, these IDs may need adjusting.
Hand MeshInstance node. This node is used to display the hand mesh that will be used when the VR controller is not holding onto a RigidBody node. The hand in the Left_Controller scene is a left hand, while the hand on the Right_Controller scene is a right hand.
Raycast is a Raycast node that is used for aiming where to teleport to when the VR controller is teleporting. The length of the Raycast is set to -16 on the Y axis and is rotated so that it points out of the pointer finger of the hand. The Raycast node has a single child node, Mesh, that is a MeshInstance. This is used for visually showing where the teleportation Raycast is aiming.
Area is a Area node will be used for grabbing RigidBody-based nodes when the VR controller grab mode is set to AREA. The Area node has a single child node, CollisionShape, that defines a sphere CollisionShape. When the VR controller is not holding any objects and the grab button is pressed, the first RigidBody-based node within the Area node will be picked up.
Position3D node called Grab_Pos. This is used to define the position that grabbed RigidBody nodes will follow then they are held by the VR controller.
Area node called Sleep_Area is used to disable sleeping for any RigidBody nodes within its CollisionShape, simple called CollisionShape. This is needed because if a RigidBody node falls asleep, then the VR controller will be unable to grab it. By using Sleep_Area, we can write code that makes any RigidBody node within it not able to sleep, therefore allowing the VR controller to grab it.
AudioStreamPlayer3D node called AudioStreamPlayer3D has a sound loaded that we will use when an object has been picked up, dropped or thrown by the VR controller. While this is not necessary for the functionality of the VR controller, it makes grabbing and dropping objects feel more natural.
Grab_Cast node and it’s only child node, Mesh. The Grab_Cast node will be used for grabbing RigidBody-based nodes when the VR controller grab mode is set to RAYCAST. This will allow the VR controller to grab objects that are just slightly out of reach using a Raycast. The Mesh node is used for visually showing where the teleportation Raycast is aiming.
That is a quick overview of how the VR controller scenes are setup, and how we will be using the nodes to provide the functionality for them. Now that we have looked at the VR controller scene, let’s write the code that will drive them.
The code for the VR controllers
Right_Controller or Left_Controller, and make a new script called VR_Controller.gd. Both scenes will be using the same script, so it doesn’t matter which you use first. With VR_Controller.gd opened, add the following code:
Tip
You can copy and paste the code from this page directly into the script editor.
If you do this, all the code copied will be using spaces instead of tabs.
Edit menu and select Convert Indent To Tabs. This will convert all the spaces into tabs. You can select Convert Indent To Spaces to convert tabs back into spaces.
GDScript
extends ARVRControllervar controller_velocity = Vector3(0,0,0)var prior_controller_position = Vector3(0,0,0)var prior_controller_velocities = []var held_object = nullvar held_object_data = {"mode":RigidBody.MODE_RIGID, "layer":1, "mask":1}var grab_areavar grab_raycastvar grab_mode = "AREA"var grab_pos_nodevar hand_meshvar hand_pickup_drop_soundvar teleport_pos = Vector3.ZEROvar teleport_meshvar teleport_button_downvar teleport_raycast# A constant to define the dead zone for both the trackpad and the joystick.# See https://web.archive.org/web/20191208161810/http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html# for more information on what dead zones are, and how we are using them in this project.const CONTROLLER_DEADZONE = 0.65const MOVEMENT_SPEED = 1.5const CONTROLLER_RUMBLE_FADE_SPEED = 2.0var directional_movement = falsefunc _ready(): # Ignore the warnings the from the connect function calls. # (We will not need the returned values for this tutorial) # warning-ignore-all:return_value_discarded teleport_raycast = get_node("RayCast") teleport_mesh = get_tree().root.get_node("Game/Teleport_Mesh") teleport_button_down = false teleport_mesh.visible = false teleport_raycast.visible = false grab_area = get_node("Area") grab_raycast = get_node("Grab_Cast") grab_pos_node = get_node("Grab_Pos") grab_mode = "AREA" grab_raycast.visible = false get_node("Sleep_Area").connect("body_entered", self, "sleep_area_entered") get_node("Sleep_Area").connect("body_exited", self, "sleep_area_exited") hand_mesh = get_node("Hand") hand_pickup_drop_sound = get_node("AudioStreamPlayer3D") connect("button_pressed", self, "button_pressed") connect("button_release", self, "button_released")func _physics_process(delta): if rumble > 0: rumble -= delta * CONTROLLER_RUMBLE_FADE_SPEED if rumble < 0: rumble = 0 if teleport_button_down == true: teleport_raycast.force_raycast_update() if teleport_raycast.is_colliding(): if teleport_raycast.get_collider() is StaticBody: if teleport_raycast.get_collision_normal().y >= 0.85: teleport_pos = teleport_raycast.get_collision_point() teleport_mesh.global_transform.origin = teleport_pos if get_is_active() == true: _physics_process_update_controller_velocity(delta) if held_object != null: var held_scale = held_object.scale held_object.global_transform = grab_pos_node.global_transform held_object.scale = held_scale _physics_process_directional_movement(delta);func _physics_process_update_controller_velocity(delta): controller_velocity = Vector3(0,0,0) if prior_controller_velocities.size() > 0: for vel in prior_controller_velocities: controller_velocity += vel controller_velocity = controller_velocity / prior_controller_velocities.size() var relative_controller_position = (global_transform.origin - prior_controller_position) controller_velocity += relative_controller_position prior_controller_velocities.append(relative_controller_position) prior_controller_position = global_transform.origin controller_velocity /= delta; if prior_controller_velocities.size() > 30: prior_controller_velocities.remove(0)func _physics_process_directional_movement(delta): var trackpad_vector = Vector2(-get_joystick_axis(1), get_joystick_axis(0)) var joystick_vector = Vector2(-get_joystick_axis(5), get_joystick_axis(4)) if trackpad_vector.length() < CONTROLLER_DEADZONE: trackpad_vector = Vector2(0,0) else: trackpad_vector = trackpad_vector.normalized() * ((trackpad_vector.length() - CONTROLLER_DEADZONE) / (1 - CONTROLLER_DEADZONE)) if joystick_vector.length() < CONTROLLER_DEADZONE: joystick_vector = Vector2(0,0) else: joystick_vector = joystick_vector.normalized() * ((joystick_vector.length() - CONTROLLER_DEADZONE) / (1 - CONTROLLER_DEADZONE)) var forward_direction = get_parent().get_node("Player_Camera").global_transform.basis.z.normalized() var right_direction = get_parent().get_node("Player_Camera").global_transform.basis.x.normalized() # Because the trackpad and the joystick will both move the player, we can add them together and normalize # the result, giving the combined movement direction var movement_vector = (trackpad_vector + joystick_vector).normalized() var movement_forward = forward_direction * movement_vector.x * delta * MOVEMENT_SPEED var movement_right = right_direction * movement_vector.y * delta * MOVEMENT_SPEED movement_forward.y = 0 movement_right.y = 0 if movement_right.length() > 0 or movement_forward.length() > 0: get_parent().global_translate(movement_right + movement_forward) directional_movement = true else: directional_movement = falsefunc button_pressed(button_index): if button_index == 15: _on_button_pressed_trigger() if button_index == 2: _on_button_pressed_grab() if button_index == 1: _on_button_pressed_menu()func _on_button_pressed_trigger(): if held_object == null: if teleport_mesh.visible == false: teleport_button_down = true teleport_mesh.visible = true teleport_raycast.visible = true else: if held_object is VR_Interactable_Rigidbody: held_object.interact()func _on_button_pressed_grab(): if teleport_button_down == true: return if held_object == null: _pickup_rigidbody() else: _throw_rigidbody() hand_pickup_drop_sound.play()func _pickup_rigidbody(): var rigid_body = null if grab_mode == "AREA": var bodies = grab_area.get_overlapping_bodies() if len(bodies) > 0: for body in bodies: if body is RigidBody: if !("NO_PICKUP" in body): rigid_body = body break elif grab_mode == "RAYCAST": grab_raycast.force_raycast_update() if grab_raycast.is_colliding(): var body = grab_raycast.get_collider() if body is RigidBody: if !("NO_PICKUP" in body): rigid_body = body if rigid_body != null: held_object = rigid_body held_object_data["mode"] = held_object.mode held_object_data["layer"] = held_object.collision_layer held_object_data["mask"] = held_object.collision_mask held_object.mode = RigidBody.MODE_STATIC held_object.collision_layer = 0 held_object.collision_mask = 0 hand_mesh.visible = false grab_raycast.visible = false if held_object is VR_Interactable_Rigidbody: held_object.controller = self held_object.picked_up()func _throw_rigidbody(): if held_object == null: return held_object.mode = held_object_data["mode"] held_object.collision_layer = held_object_data["layer"] held_object.collision_mask = held_object_data["mask"] held_object.apply_impulse(Vector3(0, 0, 0), controller_velocity) if held_object is VR_Interactable_Rigidbody: held_object.dropped() held_object.controller = null held_object = null hand_mesh.visible = true if grab_mode == "RAYCAST": grab_raycast.visible = truefunc _on_button_pressed_menu(): if grab_mode == "AREA": grab_mode = "RAYCAST" if held_object == null: grab_raycast.visible = true elif grab_mode == "RAYCAST": grab_mode = "AREA" grab_raycast.visible = falsefunc button_released(button_index): if button_index == 15: _on_button_released_trigger()func _on_button_released_trigger(): if teleport_button_down == true: if teleport_pos != null and teleport_mesh.visible == true: var camera_offset = get_parent().get_node("Player_Camera").global_transform.origin - get_parent().global_transform.origin camera_offset.y = 0 get_parent().global_transform.origin = teleport_pos - camera_offset teleport_button_down = false teleport_mesh.visible = false teleport_raycast.visible = false teleport_pos = nullfunc sleep_area_entered(body): if "can_sleep" in body: body.can_sleep = false body.sleeping = falsefunc sleep_area_exited(body): if "can_sleep" in body: # Allow the CollisionBody to sleep by setting the "can_sleep" variable to true body.can_sleep = true
This is quite a bit of code to go through. Let’s go through what the code does step-by-step.
Explaining the VR controller code
First, let’s go through all the class variables in the script:
controller_velocity: A variable to hold a rough approximation of the VR controller’s velocity.prior_controller_position: A variable to hold the VR controller’s last position in 3D space.prior_controller_velocities: An Array to hold the last 30 calculated VR controller velocities. This is used to smooth the velocity calculations over time.held_object: A variable to hold a reference to the object the VR controller is holding. If the VR controller is not holding any objects, this variable will benull.held_object_data: A dictionary to hold data for the RigidBody node being held by the VR controller. This is used to reset the RigidBody‘s data when it is no longer held.grab_area: A variable to hold the Area node used to grab objects with the VR controller.grab_raycast: A variable to hold the Raycast node used to grab objects with the VR controller.grab_mode: A variable to define the grab mode the VR controller is using. There are only two modes for grabbing objects in this tutorial,AREAandRAYCAST.grab_pos_node: A variable to hold the node that will be used to update the position and rotation of held objects.hand_mesh: A variable to hold the MeshInstance node that contains the hand mesh for the VR controller. This mesh will be shown when the VR controller is not holding anything.hand_pickup_drop_sound: A variable to hold the AudioStreamPlayer3D node that contains the pickup/drop sound.teleport_pos: A variable to hold the position the player will be teleported to when the VR controller teleports the player.teleport_mesh: A variable to hold the MeshInstance node used to show where the player is teleporting to.teleport_button_down: A variable used to track whether the controller’s teleport button is held down. This will be used to detect if this VR controller is trying to teleport the player.teleport_raycast: A variable to hold the Raycast node used to calculate the teleport position. This node also has a MeshInstance that acts as a ‘laser sight’ for aiming.CONTROLLER_DEADZONE: A constant to define the deadzone for both the trackpad and the joystick on the VR controller. See the note below for more information.MOVEMENT_SPEED: A constant to define the speed the player moves at when using the trackpad/joystick to move artificially.CONTROLLER_RUMBLE_FADE_SPEED: A constant to define how fast the VR controller rumble fades.directional_movement: A variable to hold whether this VR controller is moving the player using the touchpad/joystick. Note here. We are using a translated version of the scaled radial dead zone code provided in that article for the VR controller’s joystick/touchpad. The article is a great read, and I highly suggest giving it a look!_readyfunction.
_ready function step-by-step explanation
connect function. We will not need the returned values for this tutorial.
Raycast node we are going to use for determining the position for teleporting and assign it to the teleport_raycast variable. We then get the MeshInstance node that we will use to show where the player will be teleporting to. The node we are using for teleporting is a child of the Game scene. We do this so the teleport mesh node is not effected by changes in the VR controller, and so the teleport mesh can be used by both VR controllers.
teleport_button_down variable is set to false, teleport_mesh.visible is set to false, and teleport_raycast.visible is set to false. This sets up the variables for teleporting the player into their initial, not teleporting the player, state.
grab_area node, the grab_raycast node, and the grab_pos_node node and assigns them all to their respective variables for use later.
grab_mode is set to AREA so the VR controller will attempt to grab objects using the Area node defined in grab_area when the VR controller’s grab/grip button is pressed. We also set the grab_raycast node’s visible property to false so the ‘laser sight’ child node of grab_raycast is not visible.
body_entered and body_exited signals from the Sleep_Area node in the VR controller to the sleep_area_entered and sleep_area_exited functions. The sleep_area_entered and sleep_area_exited functions will be used to make RigidBody nodes unable to sleep when nearby the VR controller.
hand_mesh and hand_pickup_drop_sound nodes are gotten and assigned them to their respective variables for use later.
button_pressed and button_release signals in the ARVRController node, which the VR controller extends, are connected to the button_pressed and button_released functions respectively. This means that when a button on the VR controller is pressed or released, the button_pressed or button_released functions defined in this script will be called.
_physics_process function step-by-step explanation
rumble variable is more than zero. If the rumble variable, which is a property of the ARVRController node, is more than zero then the VR controller rumbles.
rumble variable is more than zero, then we reduce the rumble by CONTROLLER_RUMBLE_FADE_SPEED every second by subtracting CONTROLLER_RUMBLE_FADE_SPEED multiplied by delta. There is then a if condition to check if rumble is less than zero, which sets rumble to zero if its value is less than zero.
rumble to a value, this code will automatically make it fade over time.
teleport_button_down variable is equal to true, which means this VR controller is trying to teleport.
teleport_button_down is equal to true, we force the teleport_raycast Raycast node to update using the force_raycast_update function. The force_raycast_update function will update the properties within the Raycast node with the latest version of the physics world.
teleport_raycast collided with anything by checking of the is_colliding function in teleport_raycast is true. If the Raycast collided with something, we then check to see if the PhysicsBody the raycast collided with is a StaticBody or not. We then check to see if the collision normal vector returned by the raycast is greater than or equal to 0.85 on the Y axis.
Note
We do this because we do not want the user to be able to teleport onto RigidBody nodes and we only want the player to be able to teleport on floor-like surfaces.
teleport_pos variable to the get_collision_point function in teleport_raycast. This will assign teleport_pos to the position the raycast collided at in world space. We then move the teleport_mesh to the world position stored in teleport_pos.
This section of code will get the position the player is aiming at with the teleportation raycast and update the teleportation mesh, giving a visual update on where the user will be teleporting to when the release the teleport button.
get_is_active function, which is defined by ARVRController. If the VR controller is active, then it calls the _physics_process_update_controller_velocity function.
_physics_process_update_controller_velocity function will calculate the VR controller’s velocity through changes in position. It is not perfect, but this process gets a rough idea of the velocity of the VR controller, which is fine for the purposes of this tutorial.
held_object variable is not equal to null.
held_scale. We then set the global_transform of the held object to the global_transform of the held_object node. This will make the held object have the same position, rotation, and scale of the grab_pos_node node in world space.
scale property of the held_object node back to held_scale.
This section of code will keep the held object in the same position and rotation as the VR controller, keeping it synced with the VR controller.
_physics_process_directional_movement function. This function contains all the code for moving the player when the touchpad/joystick on the VR controller moves.
_physics_process_update_controller_velocity function step-by-step explanation
controller_velocity variable to zero Vector3.
prior_controller_velocities array. We do this by checking to see if the size() function returns a value greater than 0. If there are cached velocities within prior_controller_velocities, then we iterate through each of the stored velocities using a for loop.
controller_velocity. Once the code has gone through all of the cached velocities in prior_controller_velocities, we divide controller_velocity by the size of the prior_controller_velocities array, which will give us the combined velocity value. This helps take the previous velocities into account, making the direction of the controller’s velocity more accurate.
_physics_process function call. We do this by subtracting prior_controller_position from the global position of the VR controller, global_transform.origin. This will give us a Vector3 that points from the position in prior_controller_position to the current position of the VR controller, which we store in a variable called relative_controller_position.
controller_velocity so the latest change in position is taken into account in the velocity calculation. We then add relative_controller_position to prior_controller_velocities so it can be taken into account on the next calculation of the VR controller’s velocity.
prior_controller_position is updated with the global position of the VR controller, global_transform.origin. We then divide controller_velocity by delta so the velocity is higher, giving results like those we expect, while still being relative to the amount of time that has passed. It is not a perfect solution, but the results look decent most of the time and for the purposes of this tutorial, it is good enough.
prior_controller_velocities has more than 30 velocities cached by checking if the size() function returns a value greater than 30. If there are more than 30 cached velocities stored in prior_controller_velocities, then we simply remove the oldest cached velocity by calling the remove function and passing in a index position of 0.
_physics_process calls. While this is not perfect, it gives a decent idea of how fast the VR controller is moving in 3D space.
_physics_process_directional_movement function step-by-step explanation
Vector2 variables called trackpad_vector and joystick_vector respectively.
Note
You may need to remap the joystick and/or touchpad index values depending on your VR headset and controller. The inputs in this tutorial are the index values of a Windows Mixed Reality headset.
trackpad_vector and joystick_vector have their deadzones account for. The code for this is detailed in the article below, with slight changes as the code is converted from C# to GDScript.
trackpad_vector and joystick_vector variables have had their deadzones account for, the code then gets the forward and right direction vectors relative to the global transform of the ARVRCamera. What this does is that it gives us vectors that point forward and right relative to the rotation of the user camera, the ARVRCamera, in world space. These vectors point in the same direction of the blue and red arrows when you select an object in the Godot editor with the local space mode button enabled. The forward direction vector is stored in a variable called forward_direction, while the right direction vector is stored in a variable called right_direction.
trackpad_vector and joystick_vector variables together and normalizes the results using the normalized function. This gives us the combined movement direction of both input devices, so we can use a single Vector2 for moving the user. We assign the combined direction to a variable called movement_vector.
forward_direction. To calculate this, we multiply forward_direction by movement_vector.x, delta, and MOVEMENT_SPEED. This will give us the distance the user will move forward when the trackpad/joystick is pushed forward or backwards. We assign this to a variable called movement_forward.
right_direction. To calculate the distance the user will move right, we multiply right_direction by movement_vector.y, delta, and MOVEMENT_SPEED. This will give us the distance the user will move right when the trackpad/joystick is pushed right or left. We assign this to a variable called movement_right.
Y axis of movement_forward and movement_right by assigning their Y values to 0. We do this so the user cannot fly/fall simply by moving the trackpad or joystick. Without doing this, the player could fly in the direction they are facing.
length function on movement_right or movement_forward is greater than 0. If it is, then we need to move the user. To move the user, we perform a global translation to the ARVROrigin node using get_parent().global_translate and pass in the movement_right variable with the movement_forward variable added to it. This will move the player in the direction the trackpad/joystick is pointing, relative to the rotation of the VR headset. We also set the directional_movement variable to true so the code knows this VR controller is moving the player.
length function on movement_right or movement_forward is less than or equal to 0, then we simply set the directional_movement variable to false so the code knows this VR controller is not moving the player.
What this function ultimately does is takes the input from the VR controller’s trackpad and joystick and moves the player in the direction the player is pushing them. Movement is relative to the rotation of the VR headset, so if the player pushes forward and turns their head to the left, they will move to the left.
button_pressed function step-by-step explanation
button_index variable is passed in by the button_pressed signal in ARVRController, which we connected in the _ready function.
There are only three buttons we are looking for in this project: the trigger button, the grab/grip button, and the menu button.
Note
You may need to remap these button index values depending on your VR headset and controller. The inputs in this tutorial are the index values of a Windows Mixed Reality headset.
button_index is equal to 15, which should map to the trigger button on the VR controller. If the button pressed is the trigger button, then the _on_button_pressed_trigger function is called.
button_index is equal to 2, then the grab button was just pressed. If the button pressed is the grab button, the _on_button_pressed_grab function is called.
button_index is equal to 1, then the menu button was just pressed. If the button pressed is the menu button, the _on_button_pressed_menu function is called.
_on_button_pressed_trigger function step-by-step explanation
held_object is equal to null. If the VR controller is not holding anything, then we assume that the trigger press on the VR controller was for teleporting. We then make sure that teleport_mesh.visible is equal to false. We use this to tell if the other VR controller is trying to teleport or not, as teleport_mesh will be visible if the other VR controller is teleporting.
teleport_mesh.visible is equal to false, then we can teleport with this VR controller. We set the teleport_button_down variable to true, set teleport_mesh.visible to true, and set teleport_raycast.visible to true. This will tell the code in _physics_process that this VR controller is going to teleport, it will make the teleport_mesh visible so the user knows where the are teleporting to, and will make teleport_raycast visible to the player has a ‘laser sight’ they can use to aim the teleportation pos.
held_object is not equal to null, then the VR controller is holding something. We then check to see if the object that is being held, held_object, extends a class called VR_Interactable_Rigidbody. We have not made VR_Interactable_Rigidbody yet, but VR_Interactable_Rigidbody will be a custom class we will use on all of the special/custom RigidBody-based nodes in the project.
Tip
VR_Interactable_Rigidbody after this section!
held_object extends VR_Interactable_Rigidbody, then we call the interact function, so the held object can do whatever it is supposed to do when the trigger is pressed and the object is held by the VR controller.
_on_button_pressed_grab function step-by-step explanation
teleport_button_down is equal to true. If it is, then it calls return. We do this because we do not want the user to be able to pick up objects while teleporting.
held_object is equal to null. If the VR controller is not holding anything, then the _pickup_rigidbody function is called. If the VR controller is holding something, held_object is not equal to null, then the _throw_rigidbody function is called.
play function on hand_pickup_drop_sound.
_pickup_rigidbody function step-by-step explanation
rigid_body, which we’ll be using to store the RigidBody that the VR controller is going to pick up, assuming there is a RigidBody to pick up.
grab_mode variable is equal to AREA. If it is, then it gets all of the PhysicsBody nodes within the grab_area using the get_overlapping_bodies functions. This function will return an array of PhysicsBody nodes. We assign the array of PhysicsBody to a new variable called bodies.
bodies variable is more than 0. If it is, we go through each of the PhysicsBody nodes in bodies using a for loop.
PhysicsBody node, we check if it is, or extends, a RigidBody node using if body is RigidBody, which will return true if the PhysicsBody node is or extends the RigidBody node. If the object is a RigidBody, then we check to make sure there is not a variable/constant called NO_PICKUP defined in the body. We do this because if you want to have RigidBody nodes that cannot be picked up, all you have to do is define a constant/variable called NO_PICKUP and the VR controller will be unable to pick it up. If the RigidBody node does not have a variable/constant defined with the name NO_PICKUP, then we assign the rigid_body variable to the RigidBody node and break the for loop.
grab_area and grabs the first RigidBody node that does not have a variable/constant named NO_PICKUP and assigns it to the rigid_body variable so we can do some additional post processing later in this function.
grab_mode variable is not equal to AREA, we then check to see if it is equal to RAYCAST instead. If it is equal to RAYCAST, we force the grab_raycast node to update using the force_raycast_update function. The force_raycast_update function will update the Raycast with the latest changes in the physics world. We then check to see if the grab_raycast node collided with something using the is_colliding function, which will return true if the Raycast hit something.
grab_raycast hit something, we get the PhysicsBody node hit using the get_collider function. The code then checks to see if the node hit is a RigidBody node using if body is RigidBody, which will return true if the PhysicsBody node is or extends the RigidBody node. Then the code checks to see if the RigidBody node does not have a variable named NO_PICKUP, and if it does not, then it assigns the RigidBody node to the rigid_body variable.
grab_raycast Raycast node out and checks if it collided with a RigidBody node that does not have a variable/constant named NO_PICKUP. If it collided with a RigidBody without NO_PICKUP, it assigns the node to the rigid_body variable so we can do some additional post processing later in this function.
rigid_body is not equal to null. If rigid_body is not equal to null, then the VR controller found a RigidBody-based node that can be picked up.
held_object to the RigidBody node stored in rigid_body. We then store the RigidBody node’s mode, collision_layer, and collision_mask in held_object_data using mode, layer, and mask as keys for the respective values. This is so we can reapply them later when the object is dropped by the VR controller.
RigidBody‘s mode to MODE_STATIC, it’s collision_layer to zero, and it’s collision_mask to zero. This will make it where the held RigidBody cannot interact with other objects in the physics world when held by the VR controller.
hand_mesh MeshInstance is made invisible by setting the visible property to false. This is so the hand does not get in the way of the held object. Likewise, the grab_raycast ‘laser sight’ is made invisible by setting the visible property to false.
VR_Interactable_Rigidbody. If it does, then sets a variable called controller on held_object to self, and calls the picked_up function on held_object. While we haven’t made VR_Interactable_Rigidbody just yet, what this will do is set tell the VR_Interactable_Rigidbody class that it is being held by a VR controller, where the a reference to the controller is stored in the controller variable, through calling the picked_up function.
Tip
VR_Interactable_Rigidbody after this section!
VR_Interactable_Rigidbody.
RigidBody was found using the grab Area or Raycast, it sets it up so that it can be carried by the VR controller.
_throw_rigidbody function step-by-step explanation
held_object variable is equal to null. If it is, then it simply calls return so nothing happens. While this shouldn’t be possible, the _throw_rigidbody function should only be called if an object is held, this check helps ensure that if something strange happens, this function will react as expected.
RigidBody data back to the held object. We take the mode, layer and mask data stored in the held_object_data dictionary and reapply it to the object in held_object. This will set the RigidBody back to the state it was prior to being picked up.
apply_impulse on the held_object so that the RigidBody is thrown in the direction of the VR controller’s velocity, controller_velocity.
VR_Interactable_Rigidbody. If it does, then we call a function called dropped in held_object and set held_object.controller to null. While we have not made VR_Interactable_Rigidbody yet, but what this will do is call the droppped function so the RigidBody can do whatever it needs to do when dropped, and we set the controller variable to null so that the RigidBody knows that it is not being held.
Tip
VR_Interactable_Rigidbody after this section!
VR_Interactable_Rigidbody.
held_object extends VR_Interactable_Rigidbody or not, we then set held_object to null so the VR controller knows it is no longer holding anything. Because the VR controller is no longer holding anything, we make the hand_mesh visible by setting hand_mesh.visible to true.
grab_mode variable is set to RAYCAST, we set grab_raycast.visible to true so the ‘laser sight’ for the Raycast in grab_raycast is visible.
_on_button_pressed_menu function step-by-step explanation
grab_mode variable is equal to AREA. If it is, then it sets grab_mode to RAYCAST. It then checks to see if the VR controller is not holding anything by checking to see if held_object is equal to null. If the VR controller is not holding anything, then grab_raycast.visible is set to true so the ‘laser sight’ on the grab raycast is visible.
grab_mode variable is not equal to AREA, then it checks to see if it is equal to RAYCAST. If it is, then it sets the grab_mode to AREA and sets grab_raycast.visible to false so the ‘laser sight’ on the grab raycast is not visible.
RigidBody-based nodes when the grab/grip button is pressed. If grab_mode is set to AREA, then the Area node in grab_area will be used for detecting RigidBody nodes, while if grab_mode is set to RAYCAST the Raycast node in grab_raycast will be used for detecting RigidBody nodes.
button_released function step-by-step explanation
button_index, is equal to 15, which should map to the trigger button on the VR controller. The button_index variable is passed in by the button_release signal in ARVRController, which we connected in the _ready function.
_on_button_released_trigger function is called.
_on_button_released_trigger function step-by-step explanation
teleport_button_down variable is equal to true.
teleport_button_down variable is equal to true, the code then checks if there is a teleport position set and whether the teleport mesh is visible. It does this by checking to see if teleport_pos is not equal to null and if teleport_mesh.visible is equal to true.
ARVROrigin node, which is assumed to be the parent node of the VR controller. To calculate the offset, the global position (global_transform.origin) of the Player_Camera node has the global position of the ARVROrigin subtracted from it. This will result in a vector that points from the ARVROrigin to the ARVRCamera, which we store in a variable called camera_offset.
ARVROrigin node. Because of this, when we teleport we want to keep the offset created by room tracking so that when the player teleports, the offset created by the room tracking is not applied. Without this, if you moved in a room and then teleported, instead of appearing at the position you wanted to teleport at, your position would be offset by the amount of distance you have from the ARVROrigin node.
Y axis. We do this because we do not want to offset based on the user’s height. If we did not do this, when teleporting the player’s head would be level with the ground.
global_transform.origin) of the ARVROrigin node to the position stored in teleport_pos with camera_offset subtracted from it. This will teleport the player and remove the room tracking offset, so the user appears exactly where they want when teleporting.
teleport_button_down is set to false, teleport_mesh.visible is set to false so the mesh is invisible, teleport_raycast.visible is set to false, and teleport_pos is set to null.
sleep_area_entered function step-by-step explanation
PhysicsBody node that entered the Sleep_Area node has a variable called can_sleep. If it does, then it sets the can_sleep variable to false and sets the sleeping variable to false.
PhysicsBody nodes would not be able to be picked up by the VR controller, even if the VR controller is at the same position as the PhysicsBody node. To work around this, we simply ‘wake up’ PhysicsBody nodes that are close to the VR controller.
sleep_area_exited function step-by-step explanation
PhysicsBody node that entered the Sleep_Area node has a variable called can_sleep. If it does, then it sets the can_sleep variable to true.
RigidBody nodes that leave the Sleep_Area to sleep again, saving performance.
VR_Controller.gd to the other VR controller scene so both VR controllers have the same script.
VR_Interactable_Rigidbody, but we have not defined it yet. While we will not be using VR_Interactable_Rigidbody in this tutorial, let’s create it real quick so the project can be run.
Creating a base class for interactable VR objects
Script tab still open, create a new GDScript called VR_Interactable_Rigidbody.gd.
Tip
Script tab by pressing File -> New Script....
VR_Interactable_Rigidbody.gd open, add the following code:
GDScript
class_name VR_Interactable_Rigidbodyextends RigidBody# (Ignore the unused variable warning)# warning-ignore:unused_class_variablevar controller = nullfunc _ready(): passfunc interact(): passfunc picked_up(): passfunc dropped(): pass
Let’s quickly go through what this script.
class_name VR_Interactable_Rigidbody. What this does is that it tells Godot that this GDScript is a new class that called VR_Interactable_Rigidbody. This allows us to compare nodes against the VR_Interactable_Rigidbody class in other script files without having to load the script directly or do anything special. We can compare the class just like all of the built-in Godot classes.
controller. controller will be used to hold a reference to the VR controller that is currently holding the object. If a VR controller is not holding the object, then the controller variable will be null. The reason we need to have a reference to the VR controller is so held objects can access VR controller specific data, like controller_velocity.
_ready function is defined by Godot and all we do is simply have pass as there is nothing we need to do when the object is added to the scene in VR_Interactable_Rigidbody.
interact function is a stub function that will be called when the interact button on the VR controller, the trigger in this case, is pressed while the object is held.
Tip
RigidBody objects.
picked_up and dropped functions are stub functions that will be called when the object is picked up and dropped by the VR controller.
RigidBody objects. Now that the base class is defined, the code in the VR controller should work. Go ahead and try the game again, and you should find you can teleport around by pressing the touch pad, and can grab and throw objects using the grab/grip buttons. it may make you motion sick! One of the main reasons this can make you feel motion sick is because your vision tells you that you are moving, while your body is not moving. This conflict of signals can make the body feel sick. Let’s add a vignette shader to help reduce motion sickness while moving in VR!
Reducing motion sickness
Note
this page on the Oculus Developer Center for more information on how to implement locomotion and reducing motion sickness.
To help reduce motion sickness while moving, we are going to add a vignette effect that will only be visible while the player moves.
Game.tscn. Under the ARVROrigin node there is a child node called Movement_Vignette. This node is going to apply a simple vignette to the VR headset when the player is moving using the VR controllers. This should help reduce motion sickness.
Movement_Vignette.tscn, which you can find in the Scenes folder. The scene is just a ColorRect node with a custom shader. Feel free to look at the custom shader if you want, it is just a slightly modified version of the vignette shader you can find in the Godot demo repository.
Movement_Vignette node and create a new script called Movement_Vignette.gd. Add the following code:
GDScript
extends Controlvar controller_onevar controller_twofunc _ready(): yield(get_tree(), "idle_frame") yield(get_tree(), "idle_frame") yield(get_tree(), "idle_frame") yield(get_tree(), "idle_frame") var interface = ARVRServer.primary_interface if interface == null: set_process(false) printerr("Movement_Vignette: no VR interface found!") return rect_size = interface.get_render_targetsize() rect_position = Vector2(0,0) controller_one = get_parent().get_node("Left_Controller") controller_two = get_parent().get_node("Right_Controller") visible = falsefunc _process(_delta): if controller_one == null or controller_two == null: return if controller_one.directional_movement == true or controller_two.directional_movement == true: visible = true else: visible = false
Because this script is fairly brief, let’s quickly go over what it does.
Explaining the vignette code
controller_one and controller_two. These variables will hold references to the left and right VR controllers.
_ready function first waits for four frames using yield. The reason we are waiting four frames is because we want to ensure the VR interface is ready and accessible.
ARVRServer.primary_interface, which is assigned to a variable called interface. The code then checks to see if interface is equal to null. If interface is equal to null, then _process is disabled using set_process with a value of false.
interface is not null, then we set the rect_size of the vignette shader to the render size of the VR viewport so it takes up the entire screen. We need to do this because different VR headsets have different resolutions and aspect ratios, so we need to resize the node accordingly. We also set the rect_position of the vignette shader to zero so it is in the correct position relative to the screen.
controller_one and controller_two respectively. Finally, the vignette shader is made invisible by default by setting it’s visible property to false.
_process the code first checks if either controller_one or controller_two are equal to null. If either node is equal to null, then return is called so nothing happens.
directional_movement is equal to true in controller_one or controller_two. If either of the VR controllers are moving the player, then the vignette shader makes itself visible by setting it’s visible property to true. If neither VR controller is moving the player, so directional_movement is false in both VR controllers, than the vignette shader makes itself invisible by setting it’s visible property to false.
That is the whole script! Now that we have written the code, go ahead and try moving around with the trackpad and/or joystick. You should find that it is less motion sickness-inducing then before! Note this page on the Oculus Developer Center for more information on how to implement locomotion and reducing motion sickness.
Final notes
RigidBody-based objects. In the next part of this tutorial series, we will be creating some special RigidBody-based objects for the player to use! Warning You can download the finished project for this tutorial series on the Godot OpenVR GitHub repository, under the releases tab!
