diff --git a/.gitignore b/.gitignore index a001e38de..9f2fd2cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ /llvm /src/r_drawersasm.obj /src/r_drawersasm.o +/build_cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 277fec59c..f32d0a43c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -300,6 +300,8 @@ endif() set( LZMA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/lzma/C" ) +option( GZDOOM_USE_OPENVR "Support OpenVR API for virtual reality head mounted displays" OFF ) + if( NOT CMAKE_CROSSCOMPILING ) if( NOT CROSS_EXPORTS ) set( CROSS_EXPORTS "" ) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 387d6855f..17cd911e2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -473,6 +473,29 @@ if( NOT DYN_FLUIDSYNTH ) endif() endif() +if (GZDOOM_USE_OPENVR) + find_path(OPENVR_SDK_PATH + NAMES + headers/openvr.h + PATHS + C:/Users/cmbruns/git/openvr + ) + find_path(OPENVR_INCLUDE_DIRECTORY + NAMES + openvr.h + PATHS + ${OPENVR_SDK_PATH}/headers + ) + include_directories("${OPENVR_INCLUDE_DIRECTORY}") + find_library(OPENVR_LIBRARY + NAMES openvr_api + # TODO: Generalize for Mac and Linux and 64-bit Windows + PATHS "${OPENVR_SDK_PATH}/lib/win32/" + ) + list(APPEND ZDOOM_LIBS ${OPENVR_LIBRARY}) + add_definitions("-DUSE_OPENVR") +endif() + # Start defining source files for ZDoom set( PLAT_WIN32_SOURCES sound/mididevices/music_win_mididevice.cpp @@ -805,6 +828,7 @@ set( FASTMATH_SOURCES gl/scene/gl_vertex.cpp gl/scene/gl_spritelight.cpp gl/dynlights/gl_dynlight1.cpp + gl/dynlights/gl_dynlight1.cpp gl/system/gl_load.c gl/models/gl_models.cpp ) @@ -1006,6 +1030,7 @@ set (PCH_SOURCES gl/stereo3d/gl_stereo_leftright.cpp gl/stereo3d/scoped_view_shifter.cpp gl/stereo3d/gl_anaglyph.cpp + gl/stereo3d/gl_openvr.cpp gl/stereo3d/gl_quadstereo.cpp gl/stereo3d/gl_sidebyside3d.cpp gl/stereo3d/gl_interleaved3d.cpp diff --git a/src/gl/data/gl_matrix.h b/src/gl/data/gl_matrix.h index 3ec1f5ff4..12ef2f12b 100644 --- a/src/gl/data/gl_matrix.h +++ b/src/gl/data/gl_matrix.h @@ -62,7 +62,7 @@ class VSMatrix { void perspective(FLOATTYPE fov, FLOATTYPE ratio, FLOATTYPE nearp, FLOATTYPE farp); void ortho(FLOATTYPE left, FLOATTYPE right, FLOATTYPE bottom, FLOATTYPE top, FLOATTYPE nearp=-1.0f, FLOATTYPE farp=1.0f); void frustum(FLOATTYPE left, FLOATTYPE right, FLOATTYPE bottom, FLOATTYPE top, FLOATTYPE nearp, FLOATTYPE farp); - void copy(FLOATTYPE * pDest) + void copy(FLOATTYPE * pDest) const { memcpy(pDest, mMatrix, 16 * sizeof(FLOATTYPE)); } diff --git a/src/gl/renderer/gl_renderbuffers.cpp b/src/gl/renderer/gl_renderbuffers.cpp index 74dc36e72..e298b313b 100644 --- a/src/gl/renderer/gl_renderbuffers.cpp +++ b/src/gl/renderer/gl_renderbuffers.cpp @@ -458,7 +458,8 @@ void FGLRenderBuffers::CreateEyeBuffers(int eye) while (mEyeFBs.Size() <= unsigned(eye)) { - GLuint texture = Create2DTexture("EyeTexture", GL_RGBA16F, mWidth, mHeight); + // GL_RGBA16F, GL_RGBA16, GL_RGBA32F all do not work with OpenVR and HTC Vive + GLuint texture = Create2DTexture("EyeTexture", GL_RGBA12, mWidth, mHeight); mEyeTextures.Push(texture); mEyeFBs.Push(CreateFrameBuffer("EyeFB", texture)); } @@ -485,6 +486,7 @@ GLuint FGLRenderBuffers::Create2DTexture(const FString &name, GLuint format, int switch (format) { case GL_RGBA8: dataformat = GL_RGBA; datatype = GL_UNSIGNED_BYTE; break; + case GL_RGBA12: dataformat = GL_RGBA; datatype = GL_UNSIGNED_SHORT; break; case GL_RGBA16: dataformat = GL_RGBA; datatype = GL_UNSIGNED_SHORT; break; case GL_RGBA16F: dataformat = GL_RGBA; datatype = GL_FLOAT; break; case GL_RGBA32F: dataformat = GL_RGBA; datatype = GL_FLOAT; break; @@ -740,6 +742,10 @@ void FGLRenderBuffers::BindEyeFB(int eye, bool readBuffer) glBindFramebuffer(readBuffer ? GL_READ_FRAMEBUFFER : GL_FRAMEBUFFER, mEyeFBs[eye]); } +GLuint FGLRenderBuffers::GetEyeTextureGLHandle(int eye) const { // Needed for OpenVR API + return mEyeTextures[eye]; +} + //========================================================================== // // Shadow map texture and frame buffers diff --git a/src/gl/renderer/gl_renderbuffers.h b/src/gl/renderer/gl_renderbuffers.h index 41202b499..1e539d48f 100644 --- a/src/gl/renderer/gl_renderbuffers.h +++ b/src/gl/renderer/gl_renderbuffers.h @@ -50,6 +50,7 @@ public: void BlitToEyeTexture(int eye); void BindEyeTexture(int eye, int texunit); void BindEyeFB(int eye, bool readBuffer = false); + GLuint GetEyeTextureGLHandle(int eye) const; // Needed for OpenVR API void BindShadowMapFB(); void BindShadowMapTexture(int index); diff --git a/src/gl/stereo3d/LSMatrix.h b/src/gl/stereo3d/LSMatrix.h new file mode 100644 index 000000000..a0e401838 --- /dev/null +++ b/src/gl/stereo3d/LSMatrix.h @@ -0,0 +1,162 @@ +/* +** LSMatrix.h +** less-simple matrix class +** +**--------------------------------------------------------------------------- +** Copyright 2016 Christopher Bruns +** 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. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 THE AUTHOR 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. +**--------------------------------------------------------------------------- +** +** +*/ + + +#ifndef VR_LS_MATRIX_H_ +#define VR_LS_MATRIX_H_ + +#include "gl/data/gl_matrix.h" +#include "openvr.h" + +namespace vr { + HmdMatrix34_t; +} + +class LSVec3 +{ +public: + LSVec3(FLOATTYPE x, FLOATTYPE y, FLOATTYPE z, FLOATTYPE w=1.0f) + : x(mVec[0]), y(mVec[1]), z(mVec[2]), w(mVec[3]) + { + mVec[0] = x; + mVec[1] = y; + mVec[2] = z; + mVec[3] = w; + } + + LSVec3(const LSVec3& rhs) + : x(mVec[0]), y(mVec[1]), z(mVec[2]), w(mVec[3]) + { + *this = rhs; + } + + LSVec3& operator=(const LSVec3& rhs) { + LSVec3& lhs = *this; + for (int i = 0; i < 4; ++i) + lhs[i] = rhs[i]; + return *this; + } + + const FLOATTYPE& operator[](int i) const {return mVec[i];} + FLOATTYPE& operator[](int i) { return mVec[i]; } + + LSVec3 operator-(const LSVec3& rhs) const { + const LSVec3& lhs = *this; + LSVec3 result = *this; + for (int i = 0; i < 4; ++i) + result[i] -= rhs[i]; + return result; + } + + FLOATTYPE mVec[4]; + FLOATTYPE& x; + FLOATTYPE& y; + FLOATTYPE& z; + FLOATTYPE& w; +}; + +LSVec3 operator*(FLOATTYPE s, const LSVec3& rhs) { + LSVec3 result = rhs; + for (int i = 0; i < 4; ++i) + result[i] *= s; + return result; +} + +class LSMatrix44 : public VSMatrix +{ +public: + LSMatrix44() + { + loadIdentity(); + } + + LSMatrix44(const vr::HmdMatrix34_t& m) { + loadIdentity(); + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 4; ++j) { + (*this)[i][j] = m.m[i][j]; + } + } + } + + LSMatrix44(const VSMatrix& m) { + m.copy(mMatrix); + } + + // overload bracket operator to return one row of the matrix, so you can invoke, say, m[2][3] + FLOATTYPE* operator[](int i) {return &mMatrix[i*4];} + const FLOATTYPE* operator[](int i) const { return &mMatrix[i * 4]; } + + LSMatrix44 operator*(const VSMatrix& rhs) const { + LSMatrix44 result(*this); + result.multMatrix(rhs); + return result; + } + + LSVec3 operator*(const LSVec3& rhs) const { + const LSMatrix44& lhs = *this; + LSVec3 result(0, 0, 0, 0); + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + result[i] += lhs[i][j] * rhs[j]; + } + } + return result; + } + + LSMatrix44 getWithoutTranslation() const { + LSMatrix44 m = *this; + // Remove translation component + m[3][3] = 1.0f; + m[3][2] = m[3][1] = m[3][0] = 0.0f; + m[2][3] = m[1][3] = m[0][3] = 0.0f; + return m; + } + + LSMatrix44 transpose() const { + LSMatrix44 result; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 4; ++j) { + result[i][j] = (*this)[j][i]; + } + } + return result; + } + +}; + +#endif // VR_LS_MATRIX_H_ + + diff --git a/src/gl/stereo3d/gl_openvr.cpp b/src/gl/stereo3d/gl_openvr.cpp new file mode 100644 index 000000000..bd0aecdc9 --- /dev/null +++ b/src/gl/stereo3d/gl_openvr.cpp @@ -0,0 +1,477 @@ +/* +** gl_openvr.cpp +** Stereoscopic virtual reality mode for the HTC Vive headset +** +**--------------------------------------------------------------------------- +** Copyright 2016 Christopher Bruns +** 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. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 THE AUTHOR 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. +**--------------------------------------------------------------------------- +** +*/ + +#ifdef USE_OPENVR + +#include "gl_openvr.h" +#include "openvr.h" +#include +#include "gl/system/gl_system.h" +#include "doomtype.h" // Printf +#include "d_player.h" +#include "g_game.h" // G_Add... +#include "p_local.h" // P_TryMove +#include "r_utility.h" // viewpitch +#include "gl/renderer/gl_renderer.h" +#include "gl/renderer/gl_renderbuffers.h" +#include "g_levellocals.h" // pixelstretch +#include "math/cmath.h" +#include "c_cvars.h" +#include "LSMatrix.h" + +// For conversion between real-world and doom units +#define VERTICAL_DOOM_UNITS_PER_METER 27.0f + +EXTERN_CVAR(Int, screenblocks); + +using namespace vr; + +// feature toggles, for testing and debugging +static const bool doTrackHmdYaw = true; +static const bool doTrackHmdPitch = true; +static const bool doTrackHmdRoll = true; +static const bool doLateScheduledRotationTracking = true; +static const bool doStereoscopicViewpointOffset = true; +static const bool doRenderToDesktop = true; // mirroring to the desktop is very helpful for debugging +static const bool doRenderToHmd = true; +static const bool doTrackHmdVerticalPosition = false; // todo: +static const bool doTrackHmdHorizontalPostion = false; // todo: +static const bool doTrackVrControllerPosition = false; // todo: + +namespace s3d +{ + +/* static */ +const Stereo3DMode& OpenVRMode::getInstance() +{ + static OpenVRMode instance; + if (! instance.hmdWasFound) + return MonoView::getInstance(); + return instance; +} + +static HmdVector3d_t eulerAnglesFromQuat(HmdQuaternion_t quat) { + double q0 = quat.w; + // permute axes to make "Y" up/yaw + double q2 = quat.x; + double q3 = quat.y; + double q1 = quat.z; + + // http://stackoverflow.com/questions/18433801/converting-a-3x3-matrix-to-euler-tait-bryan-angles-pitch-yaw-roll + double roll = atan2(2 * (q0*q1 + q2*q3), 1 - 2 * (q1*q1 + q2*q2)); + double pitch = asin(2 * (q0*q2 - q3*q1)); + double yaw = atan2(2 * (q0*q3 + q1*q2), 1 - 2 * (q2*q2 + q3*q3)); + + return HmdVector3d_t{ yaw, pitch, roll }; +} + +static HmdQuaternion_t quatFromMatrix(HmdMatrix34_t matrix) { + HmdQuaternion_t q; + typedef float f34[3][4]; + f34& a = matrix.m; + // http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/ + float trace = a[0][0] + a[1][1] + a[2][2]; // I removed + 1.0f; see discussion with Ethan + if (trace > 0) {// I changed M_EPSILON to 0 + float s = 0.5f / sqrtf(trace + 1.0f); + q.w = 0.25f / s; + q.x = (a[2][1] - a[1][2]) * s; + q.y = (a[0][2] - a[2][0]) * s; + q.z = (a[1][0] - a[0][1]) * s; + } + else { + if (a[0][0] > a[1][1] && a[0][0] > a[2][2]) { + float s = 2.0f * sqrtf(1.0f + a[0][0] - a[1][1] - a[2][2]); + q.w = (a[2][1] - a[1][2]) / s; + q.x = 0.25f * s; + q.y = (a[0][1] + a[1][0]) / s; + q.z = (a[0][2] + a[2][0]) / s; + } + else if (a[1][1] > a[2][2]) { + float s = 2.0f * sqrtf(1.0f + a[1][1] - a[0][0] - a[2][2]); + q.w = (a[0][2] - a[2][0]) / s; + q.x = (a[0][1] + a[1][0]) / s; + q.y = 0.25f * s; + q.z = (a[1][2] + a[2][1]) / s; + } + else { + float s = 2.0f * sqrtf(1.0f + a[2][2] - a[0][0] - a[1][1]); + q.w = (a[1][0] - a[0][1]) / s; + q.x = (a[0][2] + a[2][0]) / s; + q.y = (a[1][2] + a[2][1]) / s; + q.z = 0.25f * s; + } + } + + return q; +} + +static HmdVector3d_t eulerAnglesFromMatrix(HmdMatrix34_t mat) { + return eulerAnglesFromQuat(quatFromMatrix(mat)); +} + +OpenVREyePose::OpenVREyePose(int eye) + : ShiftedEyePose( 0.0f ) + , eye(eye) + , eyeTexture(nullptr) + , verticalDoomUnitsPerMeter(VERTICAL_DOOM_UNITS_PER_METER) + , currentPose(nullptr) +{ +} + + +/* virtual */ +OpenVREyePose::~OpenVREyePose() +{ + dispose(); +} + +static void vSMatrixFromHmdMatrix34(VSMatrix& m1, const vr::HmdMatrix34_t& m2) +{ + float tmp[16]; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 4; ++j) { + tmp[4 * i + j] = m2.m[i][j]; + } + } + int i = 3; + for (int j = 0; j < 4; ++j) { + tmp[4 * i + j] = 0; + } + tmp[15] = 1; + m1.loadMatrix(&tmp[0]); +} + + +/* virtual */ +void OpenVREyePose::GetViewShift(FLOATTYPE yaw, FLOATTYPE outViewShift[3]) const +{ + outViewShift[0] = outViewShift[1] = outViewShift[2] = 0; + + if (currentPose == nullptr) + return; + const vr::TrackedDevicePose_t& hmd = *currentPose; + if (! hmd.bDeviceIsConnected) + return; + if (! hmd.bPoseIsValid) + return; + + if (! doStereoscopicViewpointOffset) + return; + + const vr::HmdMatrix34_t& hmdPose = hmd.mDeviceToAbsoluteTracking; + + // Pitch and Roll are identical between OpenVR and Doom worlds. + // But yaw can differ, depending on starting state, and controller movement. + float doomYawDegrees = yaw; + float openVrYawDegrees = RAD2DEG(-eulerAnglesFromMatrix(hmdPose).v[0]); + float deltaYawDegrees = doomYawDegrees - openVrYawDegrees; + while (deltaYawDegrees > 180) + deltaYawDegrees -= 360; + while (deltaYawDegrees < -180) + deltaYawDegrees += 360; + + // extract rotation component from hmd transform + LSMatrix44 openvr_X_hmd(hmdPose); + LSMatrix44 hmdRot = openvr_X_hmd.getWithoutTranslation(); // .transpose(); + + /// In these eye methods, just get local inter-eye stereoscopic shift, not full position shift /// + + // compute local eye shift + LSMatrix44 eyeShift2; + eyeShift2.loadIdentity(); + eyeShift2 = eyeShift2 * eyeToHeadTransform; // eye to head + eyeShift2 = eyeShift2 * hmdRot; // head to openvr + + LSVec3 eye_EyePos = LSVec3(0, 0, 0); // eye position in eye frame + LSVec3 hmd_EyePos = LSMatrix44(eyeToHeadTransform) * eye_EyePos; + LSVec3 hmd_HmdPos = LSVec3(0, 0, 0); // hmd position in hmd frame + LSVec3 openvr_EyePos = openvr_X_hmd * hmd_EyePos; + LSVec3 openvr_HmdPos = openvr_X_hmd * hmd_HmdPos; + LSVec3 hmd_OtherEyePos = LSMatrix44(otherEyeToHeadTransform) * eye_EyePos; + LSVec3 openvr_OtherEyePos = openvr_X_hmd * hmd_OtherEyePos; + LSVec3 openvr_EyeOffset = openvr_EyePos - openvr_HmdPos; + + VSMatrix doomInOpenVR = VSMatrix(); + doomInOpenVR.loadIdentity(); + // permute axes + float permute[] = { // Convert from OpenVR to Doom axis convention, including mirror inversion + -1, 0, 0, 0, // X-right in OpenVR -> X-left in Doom + 0, 0, 1, 0, // Z-backward in OpenVR -> Y-backward in Doom + 0, 1, 0, 0, // Y-up in OpenVR -> Z-up in Doom + 0, 0, 0, 1}; + doomInOpenVR.multMatrix(permute); + doomInOpenVR.scale(verticalDoomUnitsPerMeter, verticalDoomUnitsPerMeter, verticalDoomUnitsPerMeter); // Doom units are not meters + doomInOpenVR.scale(level.info->pixelstretch, level.info->pixelstretch, 1.0); // Doom universe is scaled by 1990s pixel aspect ratio + doomInOpenVR.rotate(deltaYawDegrees, 0, 0, 1); + + LSVec3 doom_EyeOffset = LSMatrix44(doomInOpenVR) * openvr_EyeOffset; + outViewShift[0] = doom_EyeOffset[0]; + outViewShift[1] = doom_EyeOffset[1]; + outViewShift[2] = doom_EyeOffset[2]; +} + +/* virtual */ +VSMatrix OpenVREyePose::GetProjection(FLOATTYPE fov, FLOATTYPE aspectRatio, FLOATTYPE fovRatio) const +{ + // Ignore those arguments and get the projection from the SDK + VSMatrix vs1 = ShiftedEyePose::GetProjection(fov, aspectRatio, fovRatio); + return projectionMatrix; +} + +void OpenVREyePose::initialize(vr::IVRSystem& vrsystem) +{ + float zNear = 5.0; + float zFar = 65536.0; + vr::HmdMatrix44_t projection = vrsystem.GetProjectionMatrix( + vr::EVREye(eye), zNear, zFar); + vr::HmdMatrix44_t proj_transpose; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + proj_transpose.m[i][j] = projection.m[j][i]; + } + } + projectionMatrix.loadIdentity(); + projectionMatrix.multMatrix(&proj_transpose.m[0][0]); + + vr::HmdMatrix34_t eyeToHead = vrsystem.GetEyeToHeadTransform(vr::EVREye(eye)); + vSMatrixFromHmdMatrix34(eyeToHeadTransform, eyeToHead); + vr::HmdMatrix34_t otherEyeToHead = vrsystem.GetEyeToHeadTransform(eye == Eye_Left ? Eye_Right : Eye_Left); + vSMatrixFromHmdMatrix34(otherEyeToHeadTransform, otherEyeToHead); + + if (eyeTexture == nullptr) + eyeTexture = new vr::Texture_t(); + eyeTexture->handle = nullptr; // TODO: populate this at resolve time + eyeTexture->eType = vr::TextureType_OpenGL; + eyeTexture->eColorSpace = vr::ColorSpace_Linear; +} + +void OpenVREyePose::dispose() +{ + if (eyeTexture) { + delete eyeTexture; + eyeTexture = nullptr; + } +} + +bool OpenVREyePose::submitFrame() const +{ + if (eyeTexture == nullptr) + return false; + if (vr::VRCompositor() == nullptr) + return false; + eyeTexture->handle = (void *)GLRenderer->mBuffers->GetEyeTextureGLHandle((int)eye); + vr::VRCompositor()->Submit(vr::EVREye(eye), eyeTexture); + return true; +} + + +OpenVRMode::OpenVRMode() + : vrSystem(nullptr) + , leftEyeView(vr::Eye_Left) + , rightEyeView(vr::Eye_Right) + , hmdWasFound(false) + , sceneWidth(0), sceneHeight(0) +{ + eye_ptrs.Push(&leftEyeView); // default behavior to Mono non-stereo rendering + + EVRInitError eError; + if (VR_IsHmdPresent()) + { + vrSystem = VR_Init(&eError, VRApplication_Scene); + if (eError != vr::VRInitError_None) { + std::string errMsg = VR_GetVRInitErrorAsEnglishDescription(eError); + vrSystem = nullptr; + return; + // TODO: report error + } + vrSystem->GetRecommendedRenderTargetSize(&sceneWidth, &sceneHeight); + + // OK + leftEyeView.initialize(*vrSystem); + rightEyeView.initialize(*vrSystem); + + if (!vr::VRCompositor()) + return; + + eye_ptrs.Push(&rightEyeView); // NOW we render to two eyes + hmdWasFound = true; + } +} + +/* virtual */ +// AdjustViewports() is called from within FLGRenderer::SetOutputViewport(...) +void OpenVRMode::AdjustViewports() const +{ + // Draw the 3D scene into the entire framebuffer + GLRenderer->mSceneViewport.width = sceneWidth; + GLRenderer->mSceneViewport.height = sceneHeight; + GLRenderer->mSceneViewport.left = 0; + GLRenderer->mSceneViewport.top = 0; + + GLRenderer->mScreenViewport.width = sceneWidth; + GLRenderer->mScreenViewport.height = sceneHeight; +} + +/* virtual */ +void OpenVRMode::Present() const { + // TODO: For performance, don't render to the desktop screen here + if (doRenderToDesktop) { + GLRenderer->mBuffers->BindOutputFB(); + GLRenderer->ClearBorders(); + + // Compute screen regions to use for left and right eye views + int leftWidth = GLRenderer->mOutputLetterbox.width / 2; + int rightWidth = GLRenderer->mOutputLetterbox.width - leftWidth; + GL_IRECT leftHalfScreen = GLRenderer->mOutputLetterbox; + leftHalfScreen.width = leftWidth; + GL_IRECT rightHalfScreen = GLRenderer->mOutputLetterbox; + rightHalfScreen.width = rightWidth; + rightHalfScreen.left += leftWidth; + + GLRenderer->mBuffers->BindEyeTexture(0, 0); + GLRenderer->DrawPresentTexture(leftHalfScreen, true); + GLRenderer->mBuffers->BindEyeTexture(1, 0); + GLRenderer->DrawPresentTexture(rightHalfScreen, true); + } + + if (doRenderToHmd) + { + leftEyeView.submitFrame(); + rightEyeView.submitFrame(); + } +} + +static int mAngleFromRadians(double radians) +{ + double m = std::round(65535.0 * radians / (2.0 * M_PI)); + return int(m); +} + +void OpenVRMode::updateHmdPose( + double hmdYawRadians, + double hmdPitchRadians, + double hmdRollRadians) const +{ + double hmdyaw = hmdYawRadians; + double hmdpitch = hmdPitchRadians; + double hmdroll = hmdRollRadians; + + double dYaw = 0; + if (doTrackHmdYaw) { + // Set HMD angle game state parameters for NEXT frame + static double previousYaw = 0; + static bool havePreviousYaw = false; + if (!havePreviousYaw) { + previousYaw = hmdyaw; + havePreviousYaw = true; + } + dYaw = hmdyaw - previousYaw; + G_AddViewAngle(mAngleFromRadians(-dYaw)); + previousYaw = hmdyaw; + } + + /* */ + // Pitch + if (doTrackHmdPitch) { + double hmdPitchInDoom = -atan(tan(hmdpitch) / level.info->pixelstretch); + double viewPitchInDoom = GLRenderer->mAngles.Pitch.Radians(); + double dPitch = hmdPitchInDoom - viewPitchInDoom; + G_AddViewPitch(mAngleFromRadians(-dPitch)); + } + + // Roll can be local, because it doesn't affect gameplay. + if (doTrackHmdRoll) + GLRenderer->mAngles.Roll = RAD2DEG(-hmdroll); + + // Late-schedule update to renderer angles directly, too + if (doLateScheduledRotationTracking) { + if (doTrackHmdPitch) + GLRenderer->mAngles.Pitch = RAD2DEG(-hmdpitch); + if (doTrackHmdYaw) + GLRenderer->mAngles.Yaw += RAD2DEG(dYaw); // "plus" is the correct direction + } +} + +/* virtual */ +void OpenVRMode::SetUp() const +{ + super::SetUp(); + + cachedScreenBlocks = screenblocks; + screenblocks = 12; // always be full-screen during 3D scene render + + if (vr::VRCompositor() == nullptr) + return; + + static vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]; + vr::VRCompositor()->WaitGetPoses( + poses, vr::k_unMaxTrackedDeviceCount, // current pose + nullptr, 0 // future pose? + ); + + TrackedDevicePose_t& hmdPose0 = poses[vr::k_unTrackedDeviceIndex_Hmd]; + + if (hmdPose0.bPoseIsValid) { + const vr::HmdMatrix34_t& hmdPose = hmdPose0.mDeviceToAbsoluteTracking; + HmdVector3d_t eulerAngles = eulerAnglesFromMatrix(hmdPose); + // Printf("%.1f %.1f %.1f\n", eulerAngles.v[0], eulerAngles.v[1], eulerAngles.v[2]); + updateHmdPose(eulerAngles.v[0], eulerAngles.v[1], eulerAngles.v[2]); + leftEyeView.setCurrentHmdPose(&hmdPose0); + rightEyeView.setCurrentHmdPose(&hmdPose0); + // TODO: position tracking + } +} + +/* virtual */ +void OpenVRMode::TearDown() const +{ + screenblocks = cachedScreenBlocks; + super::TearDown(); +} + +/* virtual */ +OpenVRMode::~OpenVRMode() +{ + if (vrSystem != nullptr) { + VR_Shutdown(); + vrSystem = nullptr; + leftEyeView.dispose(); + rightEyeView.dispose(); + } +} + +} /* namespace s3d */ + +#endif + diff --git a/src/gl/stereo3d/gl_openvr.h b/src/gl/stereo3d/gl_openvr.h new file mode 100644 index 000000000..9fc905207 --- /dev/null +++ b/src/gl/stereo3d/gl_openvr.h @@ -0,0 +1,110 @@ +/* +** gl_openvr.h +** Stereoscopic virtual reality mode for the HTC Vive headset +** +**--------------------------------------------------------------------------- +** Copyright 2016 Christopher Bruns +** 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. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 THE AUTHOR 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. +**--------------------------------------------------------------------------- +** +** +*/ + +#ifndef GL_OPENVR_H_ +#define GL_OPENVR_H_ + +#include "gl_stereo3d.h" +#include "gl_stereo_leftright.h" + +// forward declaration from openvr.h +namespace vr { + class IVRSystem; + struct HmdMatrix44_t; + struct Texture_t; + struct TrackedDevicePose_t; +} + +/* stereoscopic 3D API */ +namespace s3d { + +class OpenVREyePose : public ShiftedEyePose +{ +public: + OpenVREyePose(int eye); + virtual ~OpenVREyePose() override; + virtual VSMatrix GetProjection(FLOATTYPE fov, FLOATTYPE aspectRatio, FLOATTYPE fovRatio) const override; + virtual void GetViewShift(float yaw, float outViewShift[3]) const override; + + void initialize(vr::IVRSystem& vrsystem); + void dispose(); + void setCurrentHmdPose(const vr::TrackedDevicePose_t * pose) const {currentPose = pose;} + bool submitFrame() const; + +protected: + VSMatrix projectionMatrix; + VSMatrix eyeToHeadTransform; + VSMatrix otherEyeToHeadTransform; + vr::Texture_t* eyeTexture; + int eye; + + // TODO: adjust doomUnitsPerMeter according to player height + float verticalDoomUnitsPerMeter; + + mutable const vr::TrackedDevicePose_t * currentPose; +}; + +class OpenVRMode : public Stereo3DMode +{ +public: + static const Stereo3DMode& getInstance(); // Might return Mono mode, if no HMD available + + virtual ~OpenVRMode() override; + virtual void SetUp() const override; // called immediately before rendering a scene frame + virtual void TearDown() const override; // called immediately after rendering a scene frame + virtual void Present() const override; + virtual void AdjustViewports() const override; + +protected: + OpenVRMode(); + // void updateDoomViewDirection() const; + void updateHmdPose(double hmdYawRadians, double hmdPitchRadians, double hmdRollRadians) const; + + OpenVREyePose leftEyeView; + OpenVREyePose rightEyeView; + + vr::IVRSystem* vrSystem; + mutable int cachedScreenBlocks; + +private: + typedef Stereo3DMode super; + bool hmdWasFound; + uint32_t sceneWidth, sceneHeight; +}; + +} /* namespace st3d */ + + +#endif /* GL_OPENVR_H_ */ diff --git a/src/gl/stereo3d/gl_stereo_cvars.cpp b/src/gl/stereo3d/gl_stereo_cvars.cpp index 201e33590..7c87290e4 100644 --- a/src/gl/stereo3d/gl_stereo_cvars.cpp +++ b/src/gl/stereo3d/gl_stereo_cvars.cpp @@ -25,10 +25,12 @@ ** */ -#include "gl/stereo3d/gl_stereo3d.h" -#include "gl/stereo3d/gl_stereo_leftright.h" -#include "gl/stereo3d/gl_anaglyph.h" -#include "gl/stereo3d/gl_quadstereo.h" +#include "gl_stereo3d.h" +#include "gl_stereo_leftright.h" +#include "gl_anaglyph.h" +#include "gl_openvr.h" +#include "gl_quadstereo.h" +#include "gl_sidebyside3d.h" #include "gl/stereo3d/gl_sidebyside3d.h" #include "gl/stereo3d/gl_interleaved3d.h" #include "gl/system/gl_cvars.h" @@ -101,8 +103,12 @@ const Stereo3DMode& Stereo3DMode::getCurrentMode() // TODO: 8: Oculus Rift case 9: setCurrentMode(AmberBlue::getInstance(vr_ipd)); - break; - // TODO: 10: HTC Vive/OpenVR + break; +#ifdef USE_OPENVR + case 10: + setCurrentMode(OpenVRMode::getInstance()); + break; +#endif case 11: setCurrentMode(TopBottom3D::getInstance(vr_ipd)); break; diff --git a/src/gl/stereo3d/scoped_view_shifter.cpp b/src/gl/stereo3d/scoped_view_shifter.cpp index ac2b89a27..5aa1ed08d 100644 --- a/src/gl/stereo3d/scoped_view_shifter.cpp +++ b/src/gl/stereo3d/scoped_view_shifter.cpp @@ -31,7 +31,7 @@ namespace s3d { -ScopedViewShifter::ScopedViewShifter(float dxyz[3]) // in meters +ScopedViewShifter::ScopedViewShifter(float dxyz[3]) // in doom units { // save original values cachedView = r_viewpoint.Pos; diff --git a/src/gl/stereo3d/scoped_view_shifter.h b/src/gl/stereo3d/scoped_view_shifter.h index bac7f2dac..1306adec1 100644 --- a/src/gl/stereo3d/scoped_view_shifter.h +++ b/src/gl/stereo3d/scoped_view_shifter.h @@ -40,7 +40,7 @@ namespace s3d { class ScopedViewShifter { public: - ScopedViewShifter(float dxyz[3]); // in meters + ScopedViewShifter(float dxyz[3]); // in doom units ~ScopedViewShifter(); private: diff --git a/wadsrc/static/language.enu b/wadsrc/static/language.enu index 41a804ed2..4ab1de2ba 100644 --- a/wadsrc/static/language.enu +++ b/wadsrc/static/language.enu @@ -2787,6 +2787,7 @@ OPTVAL_ROWINTERLEAVED = "Row Interleaved"; OPTVAL_COLUMNINTERLEAVED = "Column Interleaved"; OPTVAL_CHECKERBOARD = "Checkerboard"; OPTVAL_QUADBUFFERED = "Quad-buffered"; +OPTVAL_OPENVR = "OpenVR-Vive"; OPTVAL_UNCHARTED2 = "Uncharted 2"; OPTVAL_HEJLDAWSON = "Hejl Dawson"; OPTVAL_REINHARD = "Reinhard"; diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index bb7c03b7a..4666a0176 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -2097,6 +2097,7 @@ OptionValue VRMode 5, "$OPTVAL_LEFTEYE" 6, "$OPTVAL_RIGHTEYE" 7, "$OPTVAL_QUADBUFFERED" + 10, "$OPTVAL_OPENVR" } OptionMenu "GLTextureGLOptions"