/*
* Copyright 2017 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.rendering.openvrprovider;
import com.sun.jna.Memory;
import com.sun.jna.NativeLibrary;
import com.sun.jna.Pointer;
import jopenvr.HmdMatrix34_t;
import jopenvr.HmdMatrix44_t;
import jopenvr.JOpenVRLibrary;
import jopenvr.JOpenVRLibrary.EVREventType;
import jopenvr.Texture_t;
import jopenvr.TrackedDevicePose_t;
import jopenvr.VRControllerState_t;
import jopenvr.VRTextureBounds_t;
import jopenvr.VR_IVRCompositor_FnTable;
import jopenvr.VR_IVROverlay_FnTable;
import jopenvr.VR_IVRSettings_FnTable;
import jopenvr.VR_IVRSystem_FnTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.utilities.NativeHelper;
import java.nio.IntBuffer;
import static org.terasology.rendering.openvrprovider.ControllerListener.LEFT_CONTROLLER;
import static org.terasology.rendering.openvrprovider.ControllerListener.RIGHT_CONTROLLER;
/**
* This class is designed to make all API calls to OpenVR, thereby insulating it from the user. If you're looking to get
* some information from the headset/controllers you should probably look at OpenVRStereoRenderer, ControllerListener,
* or OpenVRState
*/
public final class OpenVRProvider {
public static Texture_t[] texType = new Texture_t[2];
private static boolean initialized;
private static final Logger logger = LoggerFactory.getLogger(OpenVRProvider.class);
private static VR_IVRSystem_FnTable vrSystem;
private static VR_IVRCompositor_FnTable vrCompositor;
private static VR_IVROverlay_FnTable vrOverlay;
private static VR_IVRSettings_FnTable vrSettings;
private static int[] controllerDeviceIndex = new int[2];
private static VRControllerState_t.ByReference[] inputStateRefernceArray = new VRControllerState_t.ByReference[2];
private static VRControllerState_t[] controllerStateReference = new VRControllerState_t[2];
private static IntBuffer hmdErrorStore;
private static TrackedDevicePose_t.ByReference hmdTrackedDevicePoseReference;
private static TrackedDevicePose_t[] hmdTrackedDevicePoses;
private static boolean[] controllerTracking = new boolean[2];
//keyboard
private static boolean keyboardShowing;
private static boolean headIsTracking;
private static OpenVRProvider instance;
private static final OpenVRState vrState = new OpenVRState();
// TextureIDs of framebuffers for each eye
private final VRTextureBounds_t texBounds = new VRTextureBounds_t();
private float nearClip = 0.5f;
private float farClip = 500.0f;
private OpenVRProvider() {
}
// Get a singleton instance.
/**
* As a general rule, we should use this class as a singleton, because multiple instantiation
* will likely cause problems in the upstream native library. This provides a convenient method
* of using OpenVRProvider as a singleton.
*/
public static OpenVRProvider getInstance() {
if (instance == null) {
instance = new OpenVRProvider();
}
return instance;
}
/**
* Get the state of the VR system. This contains the poses of the eyes, controllers, etc...
* @return the VR state.
*/
public OpenVRState getState() {
return vrState;
}
/**
* Initialize the VR system. Note that calling this method will cause OpenVR to launch. If there is no headset
* connected, or if the OpenVR library fails to initialize for some reason, this will return false, and a log
* entry about why initialization failed will be written.
* @return true if successful.
*/
public boolean init() {
for (int handIndex = 0; handIndex < 2; handIndex++) {
controllerDeviceIndex[handIndex] = -1;
controllerStateReference[handIndex] = new VRControllerState_t();
inputStateRefernceArray[handIndex] = new VRControllerState_t.ByReference();
inputStateRefernceArray[handIndex].setAutoRead(false);
inputStateRefernceArray[handIndex].setAutoWrite(false);
inputStateRefernceArray[handIndex].setAutoSynch(false);
texType[handIndex] = new Texture_t();
}
if (!initializeOpenVRLibrary()) {
logger.warn("JOpenVR library loading failed.");
return false;
}
if (!initializeJOpenVR()) {
logger.warn("JOpenVR initialization failed.");
return false;
}
int initAttempts = 0;
boolean initSuccess = false;
// OpenVR has a race condition here - it is necessary
// to initialize the overlay, but certain operations
// that appear to take place outside of the main thread
// seem to prevent that from happening if it's done too
// soon after OpenVR is initialized. This loop waits a
// reasonable amount of time and makes several attempts.
// In my testing, it works all of the time.
while (!initSuccess && initAttempts < 10) {
try {
Thread.sleep(300);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
initSuccess = initOpenVRCompositor(true);
initAttempts++;
}
if (!initOpenVROverlay()) {
logger.warn("VROverlay initialization failed.");
return false;
}
if (!initOpenVROSettings()) {
logger.warn("OpenVR settings initialization failed.");
return false;
}
initialized = true;
return true;
}
/**
*
* @return true if initialized.
*/
public boolean isInitialized() {
return initialized;
}
/**
* In some instances, OpenVR will lose tracking on the head set. For example, if the line of sight to both light
* houses is obstructed, it is impossible to track the head set. In this case, the head set cannot be reliably
* tracked. In such cases, this method will return false, signaling that the head set tracking information returned
* by getEyePose() is unreliable.
*
* @return true if the pose of the headset is currently considered reliable.
*/
public boolean isHeadTracking() {
return headIsTracking;
}
/**
*
* @param controllerIndex - 0 for left, 1 for right, an integer.
* @return true if the pose of the controller is currently considered reliable.
*/
public boolean isControllerTrackint(int controllerIndex) {
return controllerTracking[controllerIndex];
}
/**
* Shut down the VR system.
*/
public void shutdown() {
JOpenVRLibrary.VR_ShutdownInternal();
vrSystem = null;
vrCompositor = null;
vrOverlay = null;
vrSettings = null;
initialized = false;
}
/**
* Query the VR library and update the VR state, which can then be retrieved via getState().
* This method should be called once per frame.
*/
public void updateState() {
updatePose();
pollControllers();
pollInputEvents();
}
/**
* Make the specified controller vibrate
* @param controller - the hand index, 0 for left and 1 for right, an integer.
* @param strength - the strength of the pulse - a short value from 0 - 3999.
*/
public static void triggerHapticPulse(int controller, int strength) {
if (controllerDeviceIndex[controller] == -1) {
return;
}
vrSystem.TriggerHapticPulse.apply(controllerDeviceIndex[controller], 0, (short) strength);
}
/**
* Submit the frame stored in the frame buffers for the left and right eyes to the compositor. When this method is
* called, the contents of those frame buffers will show up in the head set. This method should be called exactly
* once per frame.
*/
public void submitFrame() {
for (int nEye = 0; nEye < 2; nEye++) {
vrCompositor.Submit.apply(
nEye,
texType[nEye], null,
JOpenVRLibrary.EVRSubmitFlags.EVRSubmitFlags_Submit_Default);
}
if (vrCompositor.PostPresentHandoff != null) {
vrCompositor.PostPresentHandoff.apply();
}
}
/**
* Set the distance of the camera from the near clipping plane, in OpenGL units, as a float.
* vrProvider.getState().getProjectionMatrix(...) method.
* @param nearClipIn - the near clip to set.
*/
public void setNearClip(float nearClipIn) {
this.nearClip = nearClipIn;
}
/**
* Set the distance of the camera from the far clipping plane, in OpenGL units, as a float.
* vrProvider.getState().getProjectionMatrix(...) method.
* @param farClipIn - the near clip to set.
*/
public void setFarClip(float farClipIn) {
this.farClip = farClipIn;
}
/**
* Turn on the keyboard overlay. This is a keyboard that hovers in front of the user, that can be typed upon by
* pointing the ray extending from the top of the controller at the key the user wants to press.
* @param showingState - true or false
* @return - true if successful. If this call fails, an error is logged.
*/
public static boolean setKeyboardOverlayShowing(boolean showingState) {
int ret;
if (showingState) {
Pointer pointer = new Memory(3);
pointer.setString(0, "mc");
Pointer empty = new Memory(1);
empty.setString(0, "");
ret = vrOverlay.ShowKeyboard.apply(0, 0, pointer, 256, empty, (byte) 1, 0);
keyboardShowing = 0 == ret; //0 = no error, > 0 see EVROverlayError
if (ret != 0) {
logger.error("VR Overlay Error: " + vrOverlay.GetOverlayErrorNameFromEnum.apply(ret).getString(0));
}
} else {
try {
vrOverlay.HideKeyboard.apply();
} catch (Error e) {
logger.error("Error bringing up keyboard overlay: " + e.toString());
}
keyboardShowing = false;
}
return keyboardShowing;
}
private void pollControllers() {
for (int handIndex = 0; handIndex < 2; handIndex++) {
if (controllerDeviceIndex[handIndex] != -1) {
vrSystem.GetControllerState.apply(controllerDeviceIndex[handIndex], inputStateRefernceArray[handIndex]);
inputStateRefernceArray[handIndex].read();
controllerStateReference[handIndex] = inputStateRefernceArray[handIndex];
vrState.updateControllerButtonState(controllerStateReference);
}
}
}
private boolean initializeOpenVRLibrary() {
if (initialized) {
return true;
}
logger.info("Adding OpenVR search path: " + NativeHelper.getOpenVRLibPath());
NativeLibrary.addSearchPath("openvr_api", NativeHelper.getOpenVRLibPath());
if (jopenvr.JOpenVRLibrary.VR_IsHmdPresent() != 1) {
logger.info("VR Headset not detected.");
return false;
}
logger.info("VR Headset detected.");
return true;
}
private static boolean initializeJOpenVR() {
hmdErrorStore = IntBuffer.allocate(1);
vrSystem = null;
JOpenVRLibrary.VR_InitInternal(hmdErrorStore, JOpenVRLibrary.EVRApplicationType.EVRApplicationType_VRApplication_Scene);
if (hmdErrorStore.get(0) == 0) {
// ok, try and get the vrSystem pointer..
vrSystem = new VR_IVRSystem_FnTable(JOpenVRLibrary.VR_GetGenericInterface(JOpenVRLibrary.IVRSystem_Version, hmdErrorStore));
}
if (vrSystem == null || hmdErrorStore.get(0) != 0) {
String errorString = jopenvr.JOpenVRLibrary.VR_GetVRInitErrorAsEnglishDescription(hmdErrorStore.get(0)).getString(0);
logger.info("vrSystem initialization failed:" + errorString);
return false;
} else {
vrSystem.setAutoSynch(false);
vrSystem.read();
logger.info("OpenVR initialized & VR connected.");
hmdTrackedDevicePoseReference = new TrackedDevicePose_t.ByReference();
hmdTrackedDevicePoses = (TrackedDevicePose_t[]) hmdTrackedDevicePoseReference.toArray(JOpenVRLibrary.k_unMaxTrackedDeviceCount);
// disable all this stuff which kills performance
hmdTrackedDevicePoseReference.setAutoRead(false);
hmdTrackedDevicePoseReference.setAutoWrite(false);
hmdTrackedDevicePoseReference.setAutoSynch(false);
for (int i = 0; i < JOpenVRLibrary.k_unMaxTrackedDeviceCount; i++) {
hmdTrackedDevicePoses[i].setAutoRead(false);
hmdTrackedDevicePoses[i].setAutoWrite(false);
hmdTrackedDevicePoses[i].setAutoSynch(false);
}
}
return true;
}
// needed for in-game keyboard
private static boolean initOpenVROverlay() {
vrOverlay = new VR_IVROverlay_FnTable(JOpenVRLibrary.VR_GetGenericInterface(JOpenVRLibrary.IVROverlay_Version, hmdErrorStore));
if (hmdErrorStore.get(0) == 0) {
vrOverlay.setAutoSynch(false);
vrOverlay.read();
logger.info("OpenVR Overlay initialized OK.");
} else {
String errorString = jopenvr.JOpenVRLibrary.VR_GetVRInitErrorAsEnglishDescription(hmdErrorStore.get(0)).getString(0);
logger.info("vrOverlay initialization failed:" + errorString);
return false;
}
return true;
}
private static boolean initOpenVROSettings() {
vrSettings = new VR_IVRSettings_FnTable(JOpenVRLibrary.VR_GetGenericInterface(JOpenVRLibrary.IVRSettings_Version, hmdErrorStore));
if (hmdErrorStore.get(0) == 0) {
vrSettings.setAutoSynch(false);
vrSettings.read();
logger.info("OpenVR Settings initialized OK.");
} else {
String errorString = jopenvr.JOpenVRLibrary.VR_GetVRInitErrorAsEnglishDescription(hmdErrorStore.get(0)).getString(0);
logger.info("OpenVROSettings initialization failed:" + errorString);
return false;
}
return true;
}
private boolean initOpenVRCompositor(boolean set) {
if (set && vrSystem != null) {
vrCompositor = new VR_IVRCompositor_FnTable(JOpenVRLibrary.VR_GetGenericInterface(JOpenVRLibrary.IVRCompositor_Version, hmdErrorStore));
if (hmdErrorStore.get(0) == 0) {
logger.info("OpenVR Compositor initialized OK.");
vrCompositor.setAutoSynch(false);
vrCompositor.read();
vrCompositor.SetTrackingSpace.apply(JOpenVRLibrary.ETrackingUniverseOrigin.ETrackingUniverseOrigin_TrackingUniverseStanding);
} else {
String errorString = jopenvr.JOpenVRLibrary.VR_GetVRInitErrorAsEnglishDescription(hmdErrorStore.get(0)).getString(0);
logger.info("vrCompositor initialization failed:" + errorString);
return false;
}
}
if (vrCompositor == null) {
logger.info("Skipping VR Compositor...");
}
// left eye
texBounds.uMax = 1f;
texBounds.uMin = 0f;
texBounds.vMax = 1f;
texBounds.vMin = 0f;
texBounds.setAutoSynch(false);
texBounds.setAutoRead(false);
texBounds.setAutoWrite(false);
texBounds.write();
// texture type
for (int nEye = 0; nEye < 2; nEye++) {
texType[0].eColorSpace = JOpenVRLibrary.EColorSpace.EColorSpace_ColorSpace_Gamma;
texType[0].eType = JOpenVRLibrary.EGraphicsAPIConvention.EGraphicsAPIConvention_API_OpenGL;
texType[0].setAutoSynch(false);
texType[0].setAutoRead(false);
texType[0].setAutoWrite(false);
texType[0].handle = -1;
texType[0].write();
}
logger.info("OpenVR Compositor initialized OK.");
return true;
}
private static void findControllerDevices() {
controllerDeviceIndex[RIGHT_CONTROLLER] = -1;
controllerDeviceIndex[LEFT_CONTROLLER] = -1;
controllerDeviceIndex[RIGHT_CONTROLLER] =
vrSystem.GetTrackedDeviceIndexForControllerRole.apply(
JOpenVRLibrary.ETrackedControllerRole.ETrackedControllerRole_TrackedControllerRole_LeftHand);
controllerDeviceIndex[LEFT_CONTROLLER] =
vrSystem.GetTrackedDeviceIndexForControllerRole.apply(
JOpenVRLibrary.ETrackedControllerRole.ETrackedControllerRole_TrackedControllerRole_RightHand);
}
private static void pollInputEvents() {
jopenvr.VREvent_t event = new jopenvr.VREvent_t();
while (vrSystem.PollNextEvent.apply(event, event.size()) > 0) {
switch (event.eventType) {
case EVREventType.EVREventType_VREvent_KeyboardClosed:
//'huzzah'
keyboardShowing = false;
break;
case EVREventType.EVREventType_VREvent_KeyboardCharInput:
break;
default:
break;
}
}
}
private void updatePose() {
vrCompositor.WaitGetPoses.apply(hmdTrackedDevicePoseReference, JOpenVRLibrary.k_unMaxTrackedDeviceCount, null, 0);
for (int nDevice = 0; nDevice < JOpenVRLibrary.k_unMaxTrackedDeviceCount; ++nDevice) {
hmdTrackedDevicePoses[nDevice].read();
}
if (hmdTrackedDevicePoses[JOpenVRLibrary.k_unTrackedDeviceIndex_Hmd].bPoseIsValid != 0) {
for (int nEye = 0; nEye < 2; nEye++) {
HmdMatrix34_t matPose = vrSystem.GetEyeToHeadTransform.apply(nEye);
vrState.setEyePoseWRTHead(matPose, nEye);
HmdMatrix44_t matProjection =
vrSystem.GetProjectionMatrix.apply(nEye,
nearClip,
farClip,
JOpenVRLibrary.EGraphicsAPIConvention.EGraphicsAPIConvention_API_OpenGL);
vrState.setProjectionMatrix(matProjection, nEye);
}
vrState.setHeadPose(hmdTrackedDevicePoses[JOpenVRLibrary.k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking);
headIsTracking = true;
} else {
headIsTracking = false;
}
findControllerDevices();
for (int handIndex = 0; handIndex < 2; handIndex++) {
if (controllerDeviceIndex[handIndex] != -1) {
controllerTracking[handIndex] = true;
vrState.setControllerPose(hmdTrackedDevicePoses[controllerDeviceIndex[handIndex]].mDeviceToAbsoluteTracking, handIndex);
} else {
controllerTracking[handIndex] = false;
}
}
}
}