/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.jme3.input.vr;
import com.jme3.app.VREnvironment;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.system.jopenvr.HmdMatrix34_t;
import com.jme3.system.jopenvr.HmdMatrix44_t;
import com.jme3.system.jopenvr.JOpenVRLibrary;
import com.jme3.system.jopenvr.OpenVRUtil;
import com.jme3.system.jopenvr.TrackedDevicePose_t;
import com.jme3.system.jopenvr.VR_IVRCompositor_FnTable;
import com.jme3.system.jopenvr.VR_IVRSystem_FnTable;
import com.jme3.util.VRUtil;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.FloatByReference;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.LongByReference;
import java.nio.IntBuffer;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A class that wraps an OpenVR system.
* @author reden - phr00t - https://github.com/phr00t
* @author Julien Seinturier - (c) 2016 - JOrigin project - http:/www.jorigin.org
*/
public class OpenVR implements VRAPI {
private static final Logger logger = Logger.getLogger(OpenVR.class.getName());
private static VR_IVRCompositor_FnTable compositorFunctions;
private static VR_IVRSystem_FnTable vrsystemFunctions;
private static boolean initSuccess = false;
private static boolean flipEyes = false;
private IntBuffer hmdDisplayFrequency;
private TrackedDevicePose_t.ByReference hmdTrackedDevicePoseReference;
protected TrackedDevicePose_t[] hmdTrackedDevicePoses;
protected IntByReference hmdErrorStore;
private final Quaternion rotStore = new Quaternion();
private final Vector3f posStore = new Vector3f();
private static FloatByReference tlastVsync;
/**
* The actual frame count.
*/
public static LongByReference _tframeCount;
// for debugging latency
private int frames = 0;
protected Matrix4f[] poseMatrices;
private final Matrix4f hmdPose = Matrix4f.IDENTITY.clone();
private Matrix4f hmdProjectionLeftEye;
private Matrix4f hmdProjectionRightEye;
private Matrix4f hmdPoseLeftEye;
private Matrix4f hmdPoseRightEye;
private Vector3f hmdPoseLeftEyeVec, hmdPoseRightEyeVec, hmdSeatToStand;
private float vsyncToPhotons;
private double timePerFrame, frameCountRun;
private long frameCount;
private OpenVRInput VRinput;
private VREnvironment environment = null;
/**
* Create a new OpenVR system
* attached to the given {@link VREnvironment VR environment}.
* @param environment the VR environment to which this API is attached.
*/
public OpenVR(VREnvironment environment){
this.environment = environment;
}
@Override
public OpenVRInput getVRinput() {
return VRinput;
}
@Override
public VR_IVRSystem_FnTable getVRSystem() {
return vrsystemFunctions;
}
@Override
public VR_IVRCompositor_FnTable getCompositor() {
return compositorFunctions;
}
@Override
public String getName() {
return "OpenVR";
}
private static long latencyWaitTime = 0;
@Override
public void setFlipEyes(boolean set) {
flipEyes = set;
}
private boolean enableDebugLatency = false;
@Override
public void printLatencyInfoToConsole(boolean set) {
enableDebugLatency = set;
}
@Override
public int getDisplayFrequency() {
if( hmdDisplayFrequency == null ) return 0;
return hmdDisplayFrequency.get(0);
}
@Override
public boolean initialize() {
logger.config("Initializing OpenVR system...");
hmdErrorStore = new IntByReference();
vrsystemFunctions = null;
JOpenVRLibrary.VR_InitInternal(hmdErrorStore, JOpenVRLibrary.EVRApplicationType.EVRApplicationType_VRApplication_Scene);
if( hmdErrorStore.getValue() == 0 ) {
vrsystemFunctions = new VR_IVRSystem_FnTable(JOpenVRLibrary.VR_GetGenericInterface(JOpenVRLibrary.IVRSystem_Version, hmdErrorStore).getPointer());
}
if( vrsystemFunctions == null || hmdErrorStore.getValue() != 0 ) {
logger.severe("OpenVR Initialize Result: " + JOpenVRLibrary.VR_GetVRInitErrorAsEnglishDescription(hmdErrorStore.getValue()).getString(0));
logger.severe("Initializing OpenVR system [FAILED]");
return false;
} else {
logger.config("OpenVR initialized & VR connected.");
vrsystemFunctions.setAutoSynch(false);
vrsystemFunctions.read();
tlastVsync = new FloatByReference();
_tframeCount = new LongByReference();
hmdDisplayFrequency = IntBuffer.allocate(1);
hmdDisplayFrequency.put( (int) JOpenVRLibrary.ETrackedDeviceProperty.ETrackedDeviceProperty_Prop_DisplayFrequency_Float);
hmdTrackedDevicePoseReference = new TrackedDevicePose_t.ByReference();
hmdTrackedDevicePoses = (TrackedDevicePose_t[])hmdTrackedDevicePoseReference.toArray(JOpenVRLibrary.k_unMaxTrackedDeviceCount);
poseMatrices = new Matrix4f[JOpenVRLibrary.k_unMaxTrackedDeviceCount];
for(int i=0;i 0 ) VRUtil.sleepNanos(latencyWaitTime);
vrsystemFunctions.GetTimeSinceLastVsync.apply(tlastVsync, _tframeCount);
float fSecondsUntilPhotons = (float)timePerFrame - tlastVsync.getValue() + vsyncToPhotons;
if( enableDebugLatency ) {
if( frames == 10 ) {
System.out.println("Waited (nanos): " + Long.toString(latencyWaitTime));
System.out.println("Predict ahead time: " + Float.toString(fSecondsUntilPhotons));
}
frames = (frames + 1) % 60;
}
// handle skipping frame stuff
long nowCount = _tframeCount.getValue();
if( nowCount - frameCount > 1 ) {
// skipped a frame!
if( enableDebugLatency ) System.out.println("Frame skipped!");
frameCountRun = 0;
if( latencyWaitTime > 0 ) {
latencyWaitTime -= TimeUnit.MILLISECONDS.toNanos(1);
if( latencyWaitTime < 0 ) latencyWaitTime = 0;
}
} else if( latencyWaitTime < timePerFrame * 1000000000.0 ) {
// didn't skip a frame, lets try waiting longer to improve latency
frameCountRun++;
latencyWaitTime += Math.round(Math.pow(frameCountRun / 10.0, 2.0));
}
frameCount = nowCount;
vrsystemFunctions.GetDeviceToAbsoluteTrackingPose.apply(
environment.isSeatedExperience()?JOpenVRLibrary.ETrackingUniverseOrigin.ETrackingUniverseOrigin_TrackingUniverseSeated:
JOpenVRLibrary.ETrackingUniverseOrigin.ETrackingUniverseOrigin_TrackingUniverseStanding,
fSecondsUntilPhotons, hmdTrackedDevicePoseReference, JOpenVRLibrary.k_unMaxTrackedDeviceCount);
}
// deal with controllers being plugged in and out
// causing an invalid memory crash... skipping for now
/*boolean hasEvent = false;
while( JOpenVRLibrary.VR_IVRSystem_PollNextEvent(OpenVR.getVRSystemInstance(), tempEvent) != 0 ) {
// wait until the events are clear..
hasEvent = true;
}
if( hasEvent ) {
// an event probably changed controller state
VRInput._updateConnectedControllers();
}*/
//update controllers pose information
environment.getVRinput().updateControllerStates();
// read pose data from native
for (int nDevice = 0; nDevice < JOpenVRLibrary.k_unMaxTrackedDeviceCount; ++nDevice ){
hmdTrackedDevicePoses[nDevice].readField("bPoseIsValid");
if( hmdTrackedDevicePoses[nDevice].bPoseIsValid != 0 ){
hmdTrackedDevicePoses[nDevice].readField("mDeviceToAbsoluteTracking");
VRUtil.convertSteamVRMatrix3ToMatrix4f(hmdTrackedDevicePoses[nDevice].mDeviceToAbsoluteTracking, poseMatrices[nDevice]);
}
}
if ( hmdTrackedDevicePoses[JOpenVRLibrary.k_unTrackedDeviceIndex_Hmd].bPoseIsValid != 0 ){
hmdPose.set(poseMatrices[JOpenVRLibrary.k_unTrackedDeviceIndex_Hmd]);
} else {
hmdPose.set(Matrix4f.IDENTITY);
}
}
@Override
public Matrix4f getHMDMatrixProjectionLeftEye(Camera cam){
if( hmdProjectionLeftEye != null ) {
return hmdProjectionLeftEye;
} else if(vrsystemFunctions == null){
return cam.getProjectionMatrix();
} else {
HmdMatrix44_t mat = vrsystemFunctions.GetProjectionMatrix.apply(JOpenVRLibrary.EVREye.EVREye_Eye_Left, cam.getFrustumNear(), cam.getFrustumFar());
hmdProjectionLeftEye = new Matrix4f();
VRUtil.convertSteamVRMatrix4ToMatrix4f(mat, hmdProjectionLeftEye);
return hmdProjectionLeftEye;
}
}
@Override
public Matrix4f getHMDMatrixProjectionRightEye(Camera cam){
if( hmdProjectionRightEye != null ) {
return hmdProjectionRightEye;
} else if(vrsystemFunctions == null){
return cam.getProjectionMatrix();
} else {
HmdMatrix44_t mat = vrsystemFunctions.GetProjectionMatrix.apply(JOpenVRLibrary.EVREye.EVREye_Eye_Right, cam.getFrustumNear(), cam.getFrustumFar());
hmdProjectionRightEye = new Matrix4f();
VRUtil.convertSteamVRMatrix4ToMatrix4f(mat, hmdProjectionRightEye);
return hmdProjectionRightEye;
}
}
@Override
public Vector3f getHMDVectorPoseLeftEye() {
if( hmdPoseLeftEyeVec == null ) {
hmdPoseLeftEyeVec = getHMDMatrixPoseLeftEye().toTranslationVector();
// set default IPD if none or broken
if( hmdPoseLeftEyeVec.x <= 0.080f * -0.5f || hmdPoseLeftEyeVec.x >= 0.040f * -0.5f ) {
hmdPoseLeftEyeVec.x = 0.065f * -0.5f;
}
if( flipEyes == false ) hmdPoseLeftEyeVec.x *= -1f; // it seems these need flipping
}
return hmdPoseLeftEyeVec;
}
@Override
public Vector3f getHMDVectorPoseRightEye() {
if( hmdPoseRightEyeVec == null ) {
hmdPoseRightEyeVec = getHMDMatrixPoseRightEye().toTranslationVector();
// set default IPD if none or broken
if( hmdPoseRightEyeVec.x >= 0.080f * 0.5f || hmdPoseRightEyeVec.x <= 0.040f * 0.5f ) {
hmdPoseRightEyeVec.x = 0.065f * 0.5f;
}
if( flipEyes == false ) hmdPoseRightEyeVec.x *= -1f; // it seems these need flipping
}
return hmdPoseRightEyeVec;
}
@Override
public Vector3f getSeatedToAbsolutePosition() {
if( environment.isSeatedExperience() == false ) return Vector3f.ZERO;
if( hmdSeatToStand == null ) {
hmdSeatToStand = new Vector3f();
HmdMatrix34_t mat = vrsystemFunctions.GetSeatedZeroPoseToStandingAbsoluteTrackingPose.apply();
Matrix4f tempmat = new Matrix4f();
VRUtil.convertSteamVRMatrix3ToMatrix4f(mat, tempmat);
tempmat.toTranslationVector(hmdSeatToStand);
}
return hmdSeatToStand;
}
@Override
public Matrix4f getHMDMatrixPoseLeftEye(){
if( hmdPoseLeftEye != null ) {
return hmdPoseLeftEye;
} else if(vrsystemFunctions == null) {
return Matrix4f.IDENTITY;
} else {
HmdMatrix34_t mat = vrsystemFunctions.GetEyeToHeadTransform.apply(JOpenVRLibrary.EVREye.EVREye_Eye_Left);
hmdPoseLeftEye = new Matrix4f();
return VRUtil.convertSteamVRMatrix3ToMatrix4f(mat, hmdPoseLeftEye);
}
}
@Override
public HmdType getType() {
if( vrsystemFunctions != null ) {
Pointer str1 = new Memory(128);
Pointer str2 = new Memory(128);
String completeName = "";
vrsystemFunctions.GetStringTrackedDeviceProperty.apply(JOpenVRLibrary.k_unTrackedDeviceIndex_Hmd,
JOpenVRLibrary.ETrackedDeviceProperty.ETrackedDeviceProperty_Prop_ManufacturerName_String,
str1, 128, hmdErrorStore);
if( hmdErrorStore.getValue() == 0 ) completeName += str1.getString(0);
vrsystemFunctions.GetStringTrackedDeviceProperty.apply(JOpenVRLibrary.k_unTrackedDeviceIndex_Hmd,
JOpenVRLibrary.ETrackedDeviceProperty.ETrackedDeviceProperty_Prop_ModelNumber_String,
str2, 128, hmdErrorStore);
if( hmdErrorStore.getValue() == 0 ) completeName += " " + str2.getString(0);
if( completeName.length() > 0 ) {
completeName = completeName.toLowerCase(Locale.ENGLISH).trim();
if( completeName.contains("htc") || completeName.contains("vive") ) {
return HmdType.HTC_VIVE;
} else if( completeName.contains("osvr") ) {
return HmdType.OSVR;
} else if( completeName.contains("oculus") || completeName.contains("rift") ||
completeName.contains("dk1") || completeName.contains("dk2") || completeName.contains("cv1") ) {
return HmdType.OCULUS_RIFT;
} else if( completeName.contains("fove") ) {
return HmdType.FOVE;
} else if( completeName.contains("game") && completeName.contains("face") ) {
return HmdType.GAMEFACE;
} else if( completeName.contains("morpheus") ) {
return HmdType.MORPHEUS;
} else if( completeName.contains("gear") ) {
return HmdType.GEARVR;
} else if( completeName.contains("star") ) {
return HmdType.STARVR;
} else if( completeName.contains("null") ) {
return HmdType.NULL;
}
}
} else return HmdType.NONE;
return HmdType.OTHER;
}
@Override
public Matrix4f getHMDMatrixPoseRightEye(){
if( hmdPoseRightEye != null ) {
return hmdPoseRightEye;
} else if(vrsystemFunctions == null) {
return Matrix4f.IDENTITY;
} else {
HmdMatrix34_t mat = vrsystemFunctions.GetEyeToHeadTransform.apply(JOpenVRLibrary.EVREye.EVREye_Eye_Right);
hmdPoseRightEye = new Matrix4f();
return VRUtil.convertSteamVRMatrix3ToMatrix4f(mat, hmdPoseRightEye);
}
}
}