/**
* Copyright 2014 JogAmp Community. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of JogAmp Community.
*/
package jogamp.opengl.oculusvr;
import com.jogamp.nativewindow.util.Dimension;
import com.jogamp.nativewindow.util.DimensionImmutable;
import com.jogamp.nativewindow.util.PointImmutable;
import com.jogamp.nativewindow.util.Rectangle;
import com.jogamp.nativewindow.util.RectangleImmutable;
import com.jogamp.oculusvr.OVR;
import com.jogamp.oculusvr.OvrHmdContext;
import com.jogamp.oculusvr.ovrEyeRenderDesc;
import com.jogamp.oculusvr.ovrFovPort;
import com.jogamp.oculusvr.ovrHmdDesc;
import com.jogamp.oculusvr.ovrSizei;
import com.jogamp.oculusvr.ovrTrackingState;
import com.jogamp.opengl.math.FovHVHalves;
import com.jogamp.opengl.math.geom.Frustum;
import com.jogamp.opengl.util.stereo.LocationSensorParameter;
import com.jogamp.opengl.util.stereo.StereoDevice;
import com.jogamp.opengl.util.stereo.StereoDeviceFactory;
import com.jogamp.opengl.util.stereo.StereoDeviceRenderer;
import com.jogamp.opengl.util.stereo.StereoUtil;
public class OVRStereoDevice implements StereoDevice {
/** 1.6 up, 5 forward */
private static final float[] DEFAULT_EYE_POSITION_OFFSET = { 0.0f, 1.6f, -5.0f };
private final StereoDeviceFactory factory;
public final int deviceIndex;
private final FovHVHalves[] defaultEyeFov;
public ovrHmdDesc hmdDesc;
public OvrHmdContext handle;
private final int supportedSensorBits;
private int usedSensorBits;
private boolean sensorsStarted = false;
private final int[] eyeRenderOrder;
private final int supportedDistortionBits, recommendedDistortionBits, minimumDistortionBits;
private final String deviceName;
private final DimensionImmutable resolution;
private final int requiredRotation;
private final PointImmutable position;
private final int dkVersion;
private final LocationSensorParameter locationSensorParams;
public OVRStereoDevice(final StereoDeviceFactory factory, final ovrHmdDesc hmdDesc, final int deviceIndex) {
if( null == hmdDesc ) {
throw new IllegalArgumentException("Passed null hmdDesc");
}
final OvrHmdContext nativeContext = hmdDesc.getHandle();
if( null == nativeContext ) {
throw new IllegalArgumentException("hmdDesc has null OvrHmdContext");
}
this.factory = factory;
this.handle = nativeContext;
this.deviceIndex = deviceIndex;
this.hmdDesc = hmdDesc;
final ovrFovPort[] defaultOVREyeFov = hmdDesc.getDefaultEyeFov(0, new ovrFovPort[ovrHmdDesc.getEyeRenderOrderArrayLength()]);
defaultEyeFov = new FovHVHalves[defaultOVREyeFov.length];
for(int i=0; i<defaultEyeFov.length; i++) {
defaultEyeFov[i] = OVRUtil.getFovHV(defaultOVREyeFov[i]);
}
eyeRenderOrder = new int[ovrHmdDesc.getEyeRenderOrderArrayLength()];
hmdDesc.getEyeRenderOrder(0, eyeRenderOrder);
supportedDistortionBits = OVRUtil.ovrDistCaps2DistBits(hmdDesc.getDistortionCaps());
recommendedDistortionBits = supportedDistortionBits; // & ~StereoDeviceRenderer.DISTORTION_TIMEWARP;
minimumDistortionBits = StereoDeviceRenderer.DISTORTION_BARREL;
usedSensorBits = 0;
supportedSensorBits = OVRUtil.ovrTrackingCaps2SensorBits(hmdDesc.getTrackingCaps());
LocationSensorParameter _locationSensorParams = null;
if( StereoUtil.usesPositionSensor(supportedSensorBits)) {
try {
final FovHVHalves posFov = FovHVHalves.byRadians(hmdDesc.getCameraFrustumHFovInRadians(),
hmdDesc.getCameraFrustumVFovInRadians());
final float posZNear = hmdDesc.getCameraFrustumNearZInMeters();
final float posZFar = hmdDesc.getCameraFrustumFarZInMeters();
_locationSensorParams = new LocationSensorParameter(new Frustum.FovDesc(posFov, posZNear, posZFar));
} catch (final IllegalArgumentException iae) {
// probably zNear/zFar issue ..
System.err.println(iae.getMessage());
}
}
locationSensorParams = _locationSensorParams;
// DK1 delivers unrotated resolution in target orientation
// DK2 delivers rotated resolution in target orientation, monitor screen is rotated 90deg clockwise
deviceName = hmdDesc.getDisplayDeviceNameAsString();
final ovrSizei res = hmdDesc.getResolution();
resolution = new Dimension(res.getW(), res.getH());
final int hmdType = hmdDesc.getType();
switch( hmdType ) {
case OVR.ovrHmd_DKHD: // 4
case 5: // OVR.ovrHmd_CrystalCoveProto:
case OVR.ovrHmd_DK2: // 6
dkVersion = 2;
requiredRotation = 90;
break;
default:
dkVersion = 1;
requiredRotation = 0;
break;
}
position = OVRUtil.getVec2iAsPoint(hmdDesc.getWindowsPos());
}
@Override
public final StereoDeviceFactory getFactory() { return factory; }
@Override
public final String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("OVRStereoDevice[product "+hmdDesc.getProductNameAsString());
sb.append(", vendor "+hmdDesc.getManufacturerAsString());
sb.append(", device "+deviceName);
sb.append(", DK "+dkVersion);
sb.append(", surfaceSize "+getSurfaceSize()+", reqRotation "+requiredRotation+" ccw-deg");
sb.append(", surfacePos "+getPosition());
sb.append(", distortionBits[supported ["+StereoUtil.distortionBitsToString(getSupportedDistortionBits())+
"], recommended ["+StereoUtil.distortionBitsToString(getRecommendedDistortionBits())+
"], minimum ["+StereoUtil.distortionBitsToString(getMinimumDistortionBits())+"]]");
sb.append(", sensorBits[supported ["+StereoUtil.sensorBitsToString(getSupportedSensorBits())+
"], enabled ["+StereoUtil.sensorBitsToString(getEnabledSensorBits())+"]]");
sb.append(", "+locationSensorParams+"]");
return sb.toString();
}
@Override
public final void dispose() {
if( isValid() ) {
stopSensors();
OVR.ovrHmd_Destroy(hmdDesc);
hmdDesc = null;
handle = null;
}
}
@Override
public boolean isValid() {
return null != hmdDesc && null != handle;
}
@Override
public final PointImmutable getPosition() { return position; }
@Override
public final DimensionImmutable getSurfaceSize() { return resolution; }
@Override
public int getRequiredRotation() { return requiredRotation; }
@Override
public float[] getDefaultEyePositionOffset() { return DEFAULT_EYE_POSITION_OFFSET; }
@Override
public final FovHVHalves[] getDefaultFOV() { return defaultEyeFov; }
@Override
public final LocationSensorParameter getLocationSensorParams() { return locationSensorParams; }
@Override
public final void resetLocationSensorOrigin() {
if( isValid() && sensorsStarted && StereoUtil.usesPositionSensor(supportedSensorBits)) {
OVR.ovrHmd_RecenterPose(hmdDesc);
}
}
/* pp */ void updateUsedSensorBits(final ovrTrackingState trackingState) {
final int pre = usedSensorBits;
if( sensorsStarted && null != trackingState ) {
usedSensorBits = StereoDevice.SENSOR_ORIENTATION |
OVRUtil.ovrTrackingStats2SensorBits(trackingState.getStatusFlags());
} else {
usedSensorBits = 0;
}
if( StereoDevice.DEBUG ) {
if( pre != usedSensorBits ) {
System.err.println("XXX: Sensor Change: "+
": pre["+StereoUtil.sensorBitsToString(pre)+"]"+
" -> now["+StereoUtil.sensorBitsToString(usedSensorBits)+"]");
}
}
}
@Override
public final boolean startSensors(final int desiredSensorBits, final int requiredSensorBits) {
if( isValid() && !sensorsStarted ) {
if( requiredSensorBits != ( supportedSensorBits & requiredSensorBits ) ) {
// required sensors not available
if( StereoDevice.DEBUG ) {
System.err.println("XXX: startSensors failed: n/a required sensors ["+StereoUtil.sensorBitsToString(requiredSensorBits)+"]");
}
return false;
}
if( 0 == ( supportedSensorBits & ( requiredSensorBits | desiredSensorBits ) ) ) {
// no sensors available
if( StereoDevice.DEBUG ) {
System.err.println("XXX: startSensors failed: n/a any sensors");
}
return false;
}
// Start the sensor which provides the Rift’s pose and motion.
final int requiredTrackingCaps = OVRUtil.sensorBits2TrackingCaps(requiredSensorBits);
final int desiredTrackingCaps = requiredTrackingCaps | OVRUtil.sensorBits2TrackingCaps(desiredSensorBits);
final boolean res;
if( OVR.ovrHmd_ConfigureTracking(hmdDesc, desiredTrackingCaps, requiredTrackingCaps) ) {
sensorsStarted = true;
updateUsedSensorBits(OVR.ovrHmd_GetTrackingState(hmdDesc, 0.0));
res = true;
} else {
res = false;
}
if( StereoDevice.DEBUG ) {
System.err.println("XXX: startSensors: "+res+", started "+sensorsStarted+
": required["+StereoUtil.sensorBitsToString(requiredSensorBits)+"]"+
", desired["+StereoUtil.sensorBitsToString(desiredSensorBits)+"]"+
", enabled["+StereoUtil.sensorBitsToString(usedSensorBits)+"]");
}
return res;
} else {
// No state change -> Success
return true;
}
}
@Override
public final boolean stopSensors() {
if( isValid() && sensorsStarted ) {
OVR.ovrHmd_ConfigureTracking(hmdDesc, 0, 0); // STOP
sensorsStarted = false;
usedSensorBits = 0;
return true;
} else {
// No state change -> Success
return true;
}
}
@Override
public final boolean getSensorsStarted() { return sensorsStarted; }
@Override
public final int getSupportedSensorBits() {
return supportedSensorBits;
}
@Override
public final int getEnabledSensorBits() {
return usedSensorBits;
}
@Override
public final int[] getEyeRenderOrder() {
return eyeRenderOrder;
}
@Override
public final int getSupportedDistortionBits() {
return supportedDistortionBits;
};
@Override
public final int getRecommendedDistortionBits() {
return recommendedDistortionBits;
}
@Override
public final int getMinimumDistortionBits() {
return minimumDistortionBits;
}
@Override
public final StereoDeviceRenderer createRenderer(final int distortionBits,
final int textureCount, final float[] eyePositionOffset,
final FovHVHalves[] eyeFov, final float pixelsPerDisplayPixel, final int textureUnit) {
final ovrFovPort ovrEyeFov0 = OVRUtil.getOVRFovPort(eyeFov[0]);
final ovrFovPort ovrEyeFov1 = OVRUtil.getOVRFovPort(eyeFov[1]);
final ovrEyeRenderDesc[] eyeRenderDesc = new ovrEyeRenderDesc[2];
eyeRenderDesc[0] = OVR.ovrHmd_GetRenderDesc(hmdDesc, OVR.ovrEye_Left, ovrEyeFov0);
eyeRenderDesc[1] = OVR.ovrHmd_GetRenderDesc(hmdDesc, OVR.ovrEye_Right, ovrEyeFov1);
if( StereoDevice.DEBUG ) {
System.err.println("XXX: eyeRenderDesc[0] "+OVRUtil.toString(eyeRenderDesc[0]));
System.err.println("XXX: eyeRenderDesc[1] "+OVRUtil.toString(eyeRenderDesc[1]));
}
final DimensionImmutable eye0TextureSize = OVRUtil.getOVRSizei(OVR.ovrHmd_GetFovTextureSize(hmdDesc, OVR.ovrEye_Left, eyeRenderDesc[0].getFov(), pixelsPerDisplayPixel));
final DimensionImmutable eye1TextureSize = OVRUtil.getOVRSizei(OVR.ovrHmd_GetFovTextureSize(hmdDesc, OVR.ovrEye_Right, eyeRenderDesc[1].getFov(), pixelsPerDisplayPixel));
if( StereoDevice.DEBUG ) {
System.err.println("XXX: recommenedTex0Size "+eye0TextureSize);
System.err.println("XXX: recommenedTex1Size "+eye1TextureSize);
}
// final int maxWidth = Math.max(eye0TextureSize.getWidth(), eye1TextureSize.getWidth());
final int maxHeight = Math.max(eye0TextureSize.getHeight(), eye1TextureSize.getHeight());
final DimensionImmutable[] eyeTextureSizes = new DimensionImmutable[] { eye0TextureSize, eye1TextureSize };
final DimensionImmutable totalTextureSize = new Dimension(eye0TextureSize.getWidth() + eye1TextureSize.getWidth(), maxHeight);
if( StereoDevice.DEBUG ) {
System.err.println("XXX: textureSize Single "+eyeTextureSizes);
System.err.println("XXX: textureSize Total "+totalTextureSize);
}
final RectangleImmutable[] eyeViewports = new RectangleImmutable[2];
if( 1 == textureCount ) { // validated in ctor below!
// one big texture/FBO, viewport to target space
eyeViewports[0] = new Rectangle(0, 0,
eye0TextureSize.getWidth(),
maxHeight);
eyeViewports[1] = new Rectangle(eye0TextureSize.getWidth(), 0,
eye1TextureSize.getWidth(),
maxHeight);
} else {
// two textures/FBOs w/ postprocessing, which renders textures/FBOs into target space
eyeViewports[0] = new Rectangle(0, 0,
eye0TextureSize.getWidth(),
eye0TextureSize.getHeight());
eyeViewports[1] = new Rectangle(0, 0,
eye1TextureSize.getWidth(),
eye1TextureSize.getHeight());
}
return new OVRStereoDeviceRenderer(this, distortionBits, textureCount, eyePositionOffset,
eyeRenderDesc, eyeTextureSizes, totalTextureSize, eyeViewports, textureUnit);
}
}