/* * 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; } } } }