Getting Started with VR
This article is for anyone who wants to start developing Virtual Reality projects in UNIGINE and is highly recommended for all new users. We're going to look into the VR Sample demo to see what's inside and learn how to use it to create our own project for VR. We're also going to consider some simple examples of making modifications and extending the basic functionality of this sample.
So, let's get started!
VR Sample#
Thinking about VR developers we created the VR Sample demo enabling you to jump straight in and start creating projects of your own. It supports Oculus Rift, HTC Vive / Vive Pro out-of-the-box.
We recommend to use this demo as a basis for your VR project. Here you'll find a set of 3D models of all popular VR controllers, as well as implementation of basic mechanics such as grabbing and throwing objects, pressing buttons, opening/closing drawers and a lot more.
The sample project is created using the Component System, so the functionality of each object is determined by the components attached.
You can extend object's functionality simply by adding more components to it. For example, the laser pointer object has the following components attached:
- ObjMovable - enables grabbing and throwing an object
- ObjLaserPointer - enables casting a ray of light by the object
1. Making a Template Project#
So, ok we have a cool demo with some stuff inside, but how do we use it as a template? It's simple - just open your SDK Browser go to the Samples tab and select Demos.
Find the VR Sample in the Available section and click Install. After installation the demo will appear in the Installed section, and you can click Copy as Project to create a project based on this sample.
In the Create New Project window, that opens, enter the name for your new VR project in the corresponding field and click Create New Project.
2. Setting Up a Device and Configuring Project#
Suppose you have successfully installed your Head-Mounted Display (HMD) of choice (please visit Oculus Rift Setup or HTC Vive Setup in case you did not). In case you are having difficulties getting your HTC Vive to work, this Troubleshooting guide might be helpful.
Major VR devices are supported out-of-the-box so the only thing you should do is to make sure that the correct plugin is loaded for your HMD.
HTC Vive | Oculus Rift |
---|---|
If you run the application via UNIGINE SDK Browser, set the Stereo 3D option to HTC Vive in the Global Options tab and click Apply: To launch the plugin, specify the extern_plugin command line option on the application start-up:
|
If you run the application via UNIGINE SDK Browser, set the Stereo 3D option to Oculus Rift in the Global Options tab and click Apply: To launch the plugin, specify the extern_plugin command line option on the application start-up:
|
3. Open Project's Source#
To open your VR project in an IDE select it on the Projects tab of the UNIGINE SDK Browser and click Open Code IDE.
As the IDE opens, you can see, that the project contains a lot of different classes. This brief overview will give you a hint on what are they all about.
Don't forget to set the appropriate platform and configuration settings for your project before compiling your code in Visual Studio.
Now, we can try and build our application for the first time.
Build your application in Visual Studio (BUILD -> Build Solution) or otherwise, and launch it by selecting the project on the Projects tab of the UNIGINE SDK Browser and clicking Run.
Before running your application via the UNIGINE SDK Browser make sure, that appropriate Customize Run Options (Debug version and x64 architecture in our case) are selected, by clicking an ellipsis under the Run button.
4. Attaching Objects to HMD#
Sometimes it might be necessary to attach some object to the HMD to follow it (e.g. a hat). All movable objects (having the movable.prop property assigned) have a switch enabling this option.
For example, if you want to make a cylinder on the table attachable to the HMD, just select the corresponding node named "cylinder" in the World Hierarchy click Edit in the Reference section and enable the Can Attach to Head option.
Then select the parent node reference, and click Apply.
The same thing can be done via code at run time:
// retrieving a NodeReference named "cylinder" and getting its reference node
NodePtr node = checked_ptr_cast<NodeReference>(World::getNodeByName("cylinder"))->getReference();
// checking if this node is a movable object by trying to get its ObjMovable component
ObjMovable *obj = ComponentSystem::get()->getComponent<ObjMovable>(node);
if (obj != nullptr)
{
// making the object attachable to the HMD
obj->can_attach_to_head = 1;
}
5. Adding a New Interaction#
Suppose we want to extend the functionality of the laser pointer in our project, that we can grab, throw and use (turn on) for now, by adding an alternative use action (change material of the object being pointed at, when certain button is pressed).
So, we're going to add a new altUseIt() method to the VRInteractable class for this new action and map it to the state of a certain controller button.
VRInteractable.h
#pragma once
#include <UnigineNode.h>
#include <UniginePhysics.h>
#include "../Framework/ComponentSystem.h"
#include "Players/VRPlayer.h"
using namespace Unigine;
using namespace Math;
class VRPlayer;
class VRInteractable : public ComponentBase
{
public:
// ...
// interact methods
virtual void grabIt(VRPlayer* player, int hand_num) {}
virtual void holdIt(VRPlayer* player, int hand_num) {}
virtual void useIt(VRPlayer* player, int hand_num) {}
virtual void altuseIt(VRPlayer* player, int hand_num) {} //<-- method for new alternative use action
virtual void throwIt(VRPlayer* player, int hand_num) {}
};
Declare and implement an override of the altUseIt() method for our laser pointer in the ObjLaserPointer.h and ObjLaserPointer.cpp files respectively:
ObjLaserPointer.h
#pragma once
#include <UnigineWorld.h>
#include "../VRInteractable.h"
class ObjLaserPointer : public VRInteractable
{
public:
// ...
// interact methods
// ...
// alternative use method override
void altuseIt(VRPlayer* player, int hand_num) override;
// ...
private:
// ...
int change_material; //<-- "change material" state
// ...
};
ObjLaserPointer.cpp
// ...
void ObjLaserPointer::init()
{
// setting the "change material" state to 0
change_material = 0;
// ...
}
void ObjLaserPointer::update()
{
if (laser->isEnabled())
{
// ...
// show text
if (hit_obj && hit_obj->getProperty() && grabbed)
{
//---------CODE TO BE ADDED TO PERFORM MATERIAL SWITCHING--------------------
if (change_material)// if "alternative use" button was pressed
{
// change object's material to mesh_base
hit_obj->setMaterialPath("Unigine::mesh_base", "*");
}
//---------------------------------------------------------------------------
// ...
}
else
obj_text->setEnabled(0);
}
// unsetting the "change material" state
change_material = 0;
}
// ...
// alternative use method override
void ObjLaserPointer::altuseIt(VRPlayer* player, int hand_num)
{
// setting the "change material" state
change_material = 1;
}
// ...
Now, we're going to map this action to the state of the YB controller button. For this purpose we should modify the VRPlayer class (which is the base class for all VR players) by adding the following code to its postUpdate() method:
VRPlayer.cpp
// ...
void VRPlayer::postUpdate()
{
for (int i = 0; i < getNumHands(); i++)
{
int hand_state = getHandState(i);
if (hand_state != HAND_FREE)
{
auto &components = getGrabComponents(i);
// ...
//-------------CODE TO BE ADDED--------------------------
// alternative use of the grabbed object
if (getControllerButtonDown(i, BUTTON::YB))
{
for (int j = 0; j < components.size(); j++)
components[j]->altuseIt(this, i);
// add callback processing if necessary
}
//--------------------------------------------------------
}
}
update_button_states();
}
// ...
6. Changing Default Look of VR Controllers#
In your VR Application you might want to display hands instead of standard controllers included in the sample (e.g. for HTC Vive).
This is simple - just perform the following steps:
- Open the world in the UnigineEditor
- Select VR -> spawn_point (a Dummy Player node with the VRPlayerSpawner component) in the World Hierarchy window and go the the Node Properties section in the Parameters window.
Basic VR Player settings (the look of controllers and teleport points, teleport ray color, hand force multiplier for physical interactions, spawn point location, etc.) can be customized via the VRPlayerSpawner component or the fields of corresponding vr_player_spawner.prop property.
- Drag the desired node for each hand from the World Nodes Hierarchy window or Editor Viewport to the corresponding field of the vr_player_spawner.prop property. For example, we can make Vive controllers look like Oculus Touch by dragging oculus_touch_left and oculus_touch_right nodes to Vive Left Controller and Vive Right Controller fields respectively.
- Save the world and launch your VR application, your Vive controllers will look like Oculus Touch in VR.
The same thing can be done via code at run time:
// AppWorldLogic.cpp
NodeReferencePtr left_hand_node; // node reference to be used for the left controller
NodeReferencePtr right_hand_node; // node reference to be used for the right controller
// ...
int AppWorldLogic::init()
{
// getting the "spawn_point" node
NodePtr player_node = World::getNodeByName("spawn_point");
// creating nodes to replace default Vive controllers (you can use custom *.node files for left and right hand)
left_hand_node = NodeReference::create("/vr_template/oculus/oculus_touch_left.node");
right_hand_node = NodeReference::create("/vr_template/oculus/oculus_touch_right.node");
// checking if this node is a spawn point by trying to get its VRPlayerSpawner component
VRPlayerSpawner *player_spawner = ComponentSystem::get()->getComponent<VRPlayerSpawner>(player_node);
if (player_spawner != nullptr)
{
// setting new nodes to visualize Vive controllers
player_spawner->vive_controller_0 = left_hand_node->getNode();
player_spawner->vive_controller_1 = right_hand_node->getNode();
}
// ...
}
7. Adding a New Interactable Object#
The next step in extending the functionality of our VR Sample is adding a new interactable object.
Let's add a new type of interactable object, that we can grab, hold and throw with an additional feature: object will change its form (to a certain preset) when we grab it, and restore it back, when we release it. It will also display certain text in the console, if the corresponding option is enabled.
So, we're going to use the following components:
- ObjMovable - to enable basic grabbing and throwing functionality
- new ObjTransformer component to enable form changing and log message printing functionality
The following steps are to be performed:
- Add a new ObjTransformer class inherited from the VRInteractable. In Visual Studio we can do it by choosing Project → Add Class from the main menu, clicking Add, specifying class name and base class in the window that opens, and clicking Finish:
- Implement functionality of transformation to the specified node on grabbing, and restoring previous form on releasing a node
Below you'll find header and implementation files for our new ObjTransformer class:
ObjTransformer.h
#pragma once #include <UnigineNode.h> #include "Components/VRInteractable.h" #include "Framework/Utils.h" class ObjTransformer : public VRInteractable { public: ObjTransformer(const NodePtr &node, int num) : VRInteractable(node, num) {} virtual ~ObjTransformer() {} // property name UNIGINE_INLINE static const char* getPropertyName() { return "transformer"; } // parameters PROPERTY_PARAMETER(Toggle, show_text, 1); // Flag indicating if messages are to be printed to the console PROPERTY_PARAMETER(String, text, "TRANSFORMATION"); // Text to be printed to the console when grabbing or releasing the node PROPERTY_PARAMETER(Node, target_object); // Node to be displayed instead of the transformer-node, when it is grabbed // interact methods void grabIt(VRPlayer* player, int hand_num) override; // override grab action handler void throwIt(VRPlayer* player, int hand_num) override; // override trow action handler void holdIt(VRPlayer* player, int hand_num) override; // override hold action handler protected: void init() override; };
ObjTransformer.cpp
#include "ObjTransformer.h" REGISTER_COMPONENT( ObjTransformer ); // macro for component registration by the Component System // initialization void ObjTransformer::init(){ // hiding the target object (if any) if (target_object){ target_object->setEnabled(0); } } // grab action handler void ObjTransformer::grabIt(VRPlayer* player, int hand_num) { // if a target object is assigned, showing it, hiding the original object and displaying a message in the log if (target_object){ target_object->setEnabled(1); // hide original object's surfaces without disabling components ObjectPtr obj = checked_ptr_cast<Object>(node); for (int i = 0; i < obj->getNumSurfaces(); i++) obj->setEnabled(0, i); if (show_text) Log::message("\n Transformer's message: %s", text.get()); } } // throw action handler void ObjTransformer::throwIt(VRPlayer* player, int hand_num) { // if a target object is assigned, hiding it, and showing back the original object if (target_object){ target_object->setEnabled(0); // show original object's surfaces back ObjectPtr obj = checked_ptr_cast<Object>(node); for (int i = 0; i < obj->getNumSurfaces(); i++) obj->setEnabled(1, i); } } // hold action handler void ObjTransformer::holdIt(VRPlayer* player, int hand_num) { // changing the position of the target object target_object->setWorldPosition(player->getHandNode(hand_num)->getWorldPosition()); }
- Build your application and launch it as we did earlier, a new property file (transformer.prop) will be generated for our new component.
- Open the world in the UnigineEditor, create a new box primitive (Create -> Primitive -> Box), and place it somewhere near the table, create a sphere primitive (Create -> Primitive -> Box) to be used for transformation.
-
To add components to the box object select it and click Add New Property in the Node Properties section, then drag the movable.prop property to the new empty field that appears. Repeat the same for the transformer.prop property, and drag the sphere from the World Hierarchy window to the Target Object field.
- Save your world and close the UnigineEditor.
- Launch your application.
8. Restricting Teleportations#
By default it is possible to teleport to any point on the scene. To avoid user interaction errors in VR (e.g. teleporting into walls or ceiling) you can restrict teleportation to certain areas only. To do so perform the following:
- Create a mesh defining the area you want to restrict user teleportation to;
- Set an intersection mask to the desired surface(s) of this mesh either in the UnigineEditor or using the setIntersectionMask() method:
// defining the teleportation mask as a hexadecimal value (e.g. with only the last bit enabled) int teleport_mask = 0x80000000; // setting the teleportation mask to the MyAreaMesh object's surface with the num index MyAreaMesh->setIntersectionMask(num, teleport_mask);
- Set the same intersection mask for the teleport ray using the following method: VRPlayerVR::setTeleportationMask(teleport_mask).
Where to Go From Here#
Congratulations! Now, you know how to create your own VR project on the basis of the VR Sample demo, and extend its functionality. So, you can continue developing it on your own. There are some recommendations for you, that might be useful:
- Try to analyse the source code of the sample further and figure out how it works, use it to write your own.
- Read the Virtual Reality Best Practices article for more information and useful tips on preparing content for VR and making user experience better.
- Read the Component System article for more information on working with the Component System.
- Check out the Component System Usage Example for more details on implementing logic using the Component System.