Files
2025-01-29 10:55:49 +01:00

635 lines
19 KiB
C++

#ifdef KORE_OCULUS
#include <Kinc/vr/vrinterface.h>
#include <Kinc/graphics4/graphics.h>
#include <Kinc/graphics4/rendertarget.h>
#include <Kore/Log.h>
#include "Extras/OVR_Math.h"
#include "GL/CAPI_GLE.h"
#include "OVR_CAPI_GL.h"
#include <assert.h>
using namespace Kore;
namespace {
kinc_vr_sensor_state_t sensorStates[2];
}
struct TextureBuffer {
ovrSession Session;
ovrTextureSwapChain TextureChain;
OVR::Sizei texSize;
kinc_g4_render_target_t OVRRenderTarget;
bool render_target_initialized;
TextureBuffer(ovrSession session, bool displayableOnHmd, OVR::Sizei size, int mipLevels, unsigned char *data, int sampleCount)
: Session(session), TextureChain(nullptr), texSize(size), render_target_initialized(false) {
UNREFERENCED_PARAMETER(sampleCount);
assert(sampleCount <= 1); // The code doesn't currently handle MSAA textures.
if (displayableOnHmd) {
// This texture isn't necessarily going to be a rendertarget, but it usually is.
assert(session); // No HMD? A little odd.
assert(sampleCount == 1); // ovr_CreateSwapTextureSetD3D11 doesn't support MSAA.
ovrTextureSwapChainDesc desc = {};
desc.Type = ovrTexture_2D;
desc.ArraySize = 1;
desc.Width = size.w;
desc.Height = size.h;
desc.MipLevels = 1;
desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB;
desc.SampleCount = 1;
desc.StaticImage = ovrFalse;
ovrResult result = ovr_CreateTextureSwapChainGL(Session, &desc, &TextureChain);
int length = 0;
ovr_GetTextureSwapChainLength(session, TextureChain, &length);
if (OVR_SUCCESS(result)) {
for (int i = 0; i < length; ++i) {
GLuint chainTexId;
ovr_GetTextureSwapChainBufferGL(Session, TextureChain, i, &chainTexId);
glBindTexture(GL_TEXTURE_2D, chainTexId);
kinc_g4_render_target_init(&OVRRenderTarget, texSize.w, texSize.h, 0, false, KINC_G4_RENDER_TARGET_FORMAT_32BIT, 0, 0);
render_target_initialized = true;
}
}
}
if (mipLevels > 1) {
glGenerateMipmap(GL_TEXTURE_2D);
}
}
~TextureBuffer() {
if (TextureChain) {
ovr_DestroyTextureSwapChain(Session, TextureChain);
TextureChain = nullptr;
}
if (render_target_initialized) {
kinc_g4_render_target_destroy(&OVRRenderTarget);
}
}
OVR::Sizei GetSize() const {
return texSize;
}
void SetAndClearRenderSurface() {
GLuint curTexId;
int curIndex;
ovr_GetTextureSwapChainCurrentIndex(Session, TextureChain, &curIndex);
ovr_GetTextureSwapChainBufferGL(Session, TextureChain, curIndex, &curTexId);
if (render_target_initialized) {
kinc_g4_render_target_t *renderTargets[1] = {&OVRRenderTarget};
kinc_g4_set_render_targets(renderTargets, 1);
}
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, curTexId, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, OVRRenderTarget.impl._depthTexture, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// glEnable(GL_FRAMEBUFFER_SRGB); // TODO: too bright
}
void UnsetRenderSurface() {
glBindFramebuffer(GL_FRAMEBUFFER, OVRRenderTarget.impl._framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
}
void Commit() {
if (TextureChain) {
ovr_CommitTextureSwapChain(Session, TextureChain);
}
}
};
//-------------------------------------------------------------------------------------------
struct OGL {
HWND Window;
HDC hDC;
HGLRC WglContext;
OVR::GLEContext GLEContext;
bool Running;
bool Key[256];
int WinSizeW;
int WinSizeH;
HINSTANCE hInstance;
OGL() : Window(nullptr), hDC(nullptr), WglContext(nullptr), GLEContext(), Running(false), WinSizeW(0), WinSizeH(0), hInstance(nullptr) {
// Clear input
for (int i = 0; i < sizeof(Key) / sizeof(Key[0]); ++i)
Key[i] = false;
}
~OGL() {
ReleaseDevice();
CloseWindow();
}
bool InitWindow(HINSTANCE hInst, const char *title, const char *windowClassName) {
hInstance = hInst;
Running = true;
// Adjust the window size and show at InitDevice time
wchar_t wchTitle[256];
MultiByteToWideChar(CP_ACP, 0, title, -1, wchTitle, 256);
wchar_t wchClassName[256];
MultiByteToWideChar(CP_ACP, 0, windowClassName, -1, wchClassName, 256);
Window = CreateWindowW(wchClassName, wchTitle, WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, 0, 0, hInst, 0);
if (!Window)
return false;
SetWindowLongPtr(Window, 0, LONG_PTR(this));
hDC = GetDC(Window);
return true;
}
void CloseWindow() {
if (Window) {
if (hDC) {
ReleaseDC(Window, hDC);
hDC = nullptr;
}
Window = nullptr;
}
}
// Note: currently there is no way to get GL to use the passed pLuid
bool InitDevice(int vpW, int vpH, const LUID * /*pLuid*/, bool windowed = true) {
UNREFERENCED_PARAMETER(windowed);
WinSizeW = vpW;
WinSizeH = vpH;
RECT size = {0, 0, vpW, vpH};
AdjustWindowRect(&size, WS_OVERLAPPEDWINDOW, false);
const UINT flags = SWP_NOMOVE | SWP_NOZORDER | SWP_SHOWWINDOW;
if (!SetWindowPos(Window, nullptr, 0, 0, size.right - size.left, size.bottom - size.top, flags))
return false;
PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARBFunc = nullptr;
PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARBFunc = nullptr;
{
// First create a context for the purpose of getting access to wglChoosePixelFormatARB / wglCreateContextAttribsARB.
PIXELFORMATDESCRIPTOR pfd;
memset(&pfd, 0, sizeof(pfd));
pfd.nSize = sizeof(pfd);
pfd.nVersion = 1;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
pfd.cColorBits = 32;
pfd.cDepthBits = 16;
int pf = ChoosePixelFormat(hDC, &pfd);
if (!pf) {
log(Warning, "Failed to choose pixel format.");
ReleaseDC(Window, hDC);
return false;
}
if (!SetPixelFormat(hDC, pf, &pfd)) {
log(Warning, "Failed to set pixel format.");
ReleaseDC(Window, hDC);
return false;
}
HGLRC context = wglCreateContext(hDC);
if (!context) {
log(Warning, "wglCreateContextfailed.");
ReleaseDC(Window, hDC);
return false;
}
if (!wglMakeCurrent(hDC, context)) {
log(Warning, "wglMakeCurrent failed.");
wglDeleteContext(context);
ReleaseDC(Window, hDC);
return false;
}
wglChoosePixelFormatARBFunc = (PFNWGLCHOOSEPIXELFORMATARBPROC)wglGetProcAddress("wglChoosePixelFormatARB");
wglCreateContextAttribsARBFunc = (PFNWGLCREATECONTEXTATTRIBSARBPROC)wglGetProcAddress("wglCreateContextAttribsARB");
assert(wglChoosePixelFormatARBFunc && wglCreateContextAttribsARBFunc);
wglDeleteContext(context);
}
// Now create the real context that we will be using.
int iAttributes[] = {// WGL_DRAW_TO_WINDOW_ARB, GL_TRUE,
WGL_SUPPORT_OPENGL_ARB,
GL_TRUE,
WGL_COLOR_BITS_ARB,
32,
WGL_DEPTH_BITS_ARB,
16,
WGL_DOUBLE_BUFFER_ARB,
GL_TRUE,
WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB,
GL_TRUE,
0,
0};
float fAttributes[] = {0, 0};
int pf = 0;
UINT numFormats = 0;
if (!wglChoosePixelFormatARBFunc(hDC, iAttributes, fAttributes, 1, &pf, &numFormats)) {
log(Warning, "wglChoosePixelFormatARBFunc failed.");
ReleaseDC(Window, hDC);
return false;
}
PIXELFORMATDESCRIPTOR pfd;
memset(&pfd, 0, sizeof(pfd));
if (!SetPixelFormat(hDC, pf, &pfd)) {
log(Warning, "SetPixelFormat failed.");
ReleaseDC(Window, hDC);
return false;
}
GLint attribs[16];
int attribCount = 0;
attribs[attribCount] = 0;
WglContext = wglCreateContextAttribsARBFunc(hDC, 0, attribs);
if (!wglMakeCurrent(hDC, WglContext)) {
log(Warning, "wglMakeCurrent failed.");
wglDeleteContext(WglContext);
ReleaseDC(Window, hDC);
return false;
}
OVR::GLEContext::SetCurrentContext(&GLEContext);
GLEContext.Init();
glEnable(GL_DEPTH_TEST);
glFrontFace(GL_CW);
glEnable(GL_CULL_FACE);
return true;
}
void ReleaseDevice() {
if (WglContext) {
wglMakeCurrent(NULL, NULL);
wglDeleteContext(WglContext);
WglContext = nullptr;
}
GLEContext.Shutdown();
}
};
namespace {
TextureBuffer *eyeRenderTexture[2] = {nullptr, nullptr};
ovrMirrorTexture mirrorTexture = nullptr;
uint mirrorFBO = 0;
long long frameIndex = 0;
bool isVisible = true;
ovrSession session;
ovrHmdDesc hmdDesc;
ovrPosef EyeRenderPose[2];
double sensorSampleTime;
OGL Platform;
void done() {
if (mirrorFBO)
glDeleteFramebuffers(1, &mirrorFBO);
if (mirrorTexture)
ovr_DestroyMirrorTexture(session, mirrorTexture);
for (int eye = 0; eye < 2; ++eye) {
delete eyeRenderTexture[eye];
}
Platform.ReleaseDevice();
ovr_Destroy(session);
}
void createOculusTexture() {
// Make eye render buffers
for (int eye = 0; eye < 2; ++eye) {
ovrSizei idealTextureSize = ovr_GetFovTextureSize(session, ovrEyeType(eye), hmdDesc.DefaultEyeFov[eye], 1);
eyeRenderTexture[eye] = new TextureBuffer(session, true, idealTextureSize, 1, NULL, 1);
if (!eyeRenderTexture[eye]->TextureChain) {
log(Info, "Failed to create texture.");
done();
}
}
ovrMirrorTextureDesc desc;
memset(&desc, 0, sizeof(desc));
desc.Width = Platform.WinSizeW;
desc.Height = Platform.WinSizeH;
desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB;
// Create mirror texture and an FBO used to copy mirror texture to back buffer
ovrResult result = ovr_CreateMirrorTextureWithOptionsGL(session, &desc, &mirrorTexture);
if (!OVR_SUCCESS(result)) {
log(Info, "Failed to create mirror texture.");
done();
}
// Configure the mirror read buffer
GLuint texId;
ovr_GetMirrorTextureBufferGL(session, mirrorTexture, &texId);
glGenFramebuffers(1, &mirrorFBO);
glBindFramebuffer(GL_READ_FRAMEBUFFER, mirrorFBO);
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId, 0);
glFramebufferRenderbuffer(GL_READ_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
}
}
void *kinc_vr_interface_init(void *hinst, const char *title, const char *windowClassName) {
ovrInitParams initParams = {ovrInit_RequestVersion, OVR_MINOR_VERSION, NULL, 0, 0};
ovrResult result = ovr_Initialize(&initParams);
if (!OVR_SUCCESS(result)) {
log(Warning, "Failed to initialize libOVR.");
return (0);
}
if (!Platform.InitWindow((HINSTANCE)hinst, title, windowClassName)) {
log(Warning, "Failed to open window.");
return (0);
}
ovrGraphicsLuid luid;
result = ovr_Create(&session, &luid);
if (!OVR_SUCCESS(result)) {
log(Info, "HMD not connected.");
return false; // TODO: retry
}
hmdDesc = ovr_GetHmdDesc(session);
// Setup Window and Graphics
// Note: the mirror window can be any size, for this sample we use 1/2 the HMD resolution
ovrSizei windowSize = {hmdDesc.Resolution.w / 2, hmdDesc.Resolution.h / 2};
if (!Platform.InitDevice(windowSize.w, windowSize.h, reinterpret_cast<LUID *>(&luid))) {
log(Info, "Failed to init device.");
done();
}
// FloorLevel will give tracking poses where the floor height is 0
ovr_SetTrackingOriginType(session, ovrTrackingOrigin_FloorLevel);
// Return window
return Platform.Window;
}
void kinc_vr_interface_begin() {
// Call ovr_GetRenderDesc each frame to get the ovrEyeRenderDesc, as the returned values (e.g. HmdToEyeOffset) may change at runtime.
ovrEyeRenderDesc eyeRenderDesc[2];
eyeRenderDesc[0] = ovr_GetRenderDesc(session, ovrEye_Left, hmdDesc.DefaultEyeFov[0]);
eyeRenderDesc[1] = ovr_GetRenderDesc(session, ovrEye_Right, hmdDesc.DefaultEyeFov[1]);
// Get both eye poses simultaneously, with IPD offset already included.
ovrPosef HmdToEyePose[2] = {eyeRenderDesc[0].HmdToEyePose, eyeRenderDesc[1].HmdToEyePose};
ovr_GetEyePoses(session, frameIndex, ovrTrue, HmdToEyePose, EyeRenderPose, &sensorSampleTime);
}
void kinc_vr_interface_begin_render(int eye) {
if (eyeRenderTexture[0] == nullptr || eyeRenderTexture[1] == nullptr)
createOculusTexture();
// Switch to eye render target
eyeRenderTexture[eye]->SetAndClearRenderSurface();
}
void kinc_vr_interface_end_render(int eye) {
// Avoids an error when calling SetAndClearRenderSurface during next iteration.
eyeRenderTexture[eye]->UnsetRenderSurface();
// Commit changes to the textures so they get picked up frame
eyeRenderTexture[eye]->Commit();
}
namespace {
kinc_matrix4x4_t convert(OVR::Matrix4f &m) {
kinc_matrix4x4_t mat;
kinc_matrix4x4_set(&mat, 0, 0, m.M[0][0]);
kinc_matrix4x4_set(&mat, 0, 1, m.M[0][1]);
kinc_matrix4x4_set(&mat, 0, 2, m.M[0][2]);
kinc_matrix4x4_set(&mat, 0, 3, m.M[0][3]);
kinc_matrix4x4_set(&mat, 1, 0, m.M[1][0]);
kinc_matrix4x4_set(&mat, 1, 1, m.M[1][1]);
kinc_matrix4x4_set(&mat, 1, 2, m.M[1][2]);
kinc_matrix4x4_set(&mat, 1, 3, m.M[1][3]);
kinc_matrix4x4_set(&mat, 2, 0, m.M[2][0]);
kinc_matrix4x4_set(&mat, 2, 1, m.M[2][1]);
kinc_matrix4x4_set(&mat, 2, 2, m.M[2][2]);
kinc_matrix4x4_set(&mat, 2, 3, m.M[2][3]);
kinc_matrix4x4_set(&mat, 3, 0, m.M[3][0]);
kinc_matrix4x4_set(&mat, 3, 1, m.M[3][1]);
kinc_matrix4x4_set(&mat, 3, 2, m.M[3][2]);
kinc_matrix4x4_set(&mat, 3, 3, m.M[3][3]);
return mat;
}
// Vector<float, 3> vectorConvert(const OVR::Vector3f& v) {
// return { v.x, v.y, v.z };
// }
// OVR::Vector3f vectorConvert(const Vector<float, 3>& v) {
// return { v.x(), v.y(), v.z() };
// }
}
/*SensorState VrInterface::getSensorState(int eye, Kore::Vector<float, 3>& headPosition) {
VrPoseState poseState;
ovrQuatf orientation = EyeRenderPose[eye].Orientation;
poseState.vrPose.orientation = Quaternion(orientation.x, orientation.y, orientation.z, orientation.w);
ovrVector3f pos = EyeRenderPose[eye].Position;
poseState.vrPose.position = vec3(pos.x, pos.y, pos.z);
ovrFovPort fov = hmdDesc.DefaultEyeFov[eye];
poseState.vrPose.left = fov.LeftTan;
poseState.vrPose.right = fov.RightTan;
poseState.vrPose.bottom = fov.DownTan;
poseState.vrPose.top = fov.UpTan;
// Get view and projection matrices
OVR::Matrix4f finalRollPitchYaw = OVR::Matrix4f(EyeRenderPose[eye].Orientation);
OVR::Vector3f finalUp = finalRollPitchYaw.Transform(OVR::Vector3f(0, 1, 0));
OVR::Vector3f finalForward = finalRollPitchYaw.Transform(OVR::Vector3f(0, 0, -1));
Kore::Vector<float, 3> right = vectorConvert(finalForward).cross(vectorConvert(finalUp)).normalize() * 0.01f;
OVR::Vector3f shiftedEyePos;
if (eye == 0) {
shiftedEyePos = vectorConvert(headPosition - right);
} else {
shiftedEyePos = vectorConvert(headPosition + right);
}
OVR::Matrix4f view = OVR::Matrix4f::LookAtRH(shiftedEyePos, shiftedEyePos + finalForward, finalUp);
OVR::Matrix4f proj = ovrMatrix4f_Projection(hmdDesc.MaxEyeFov[eye], 0.2f, 1000.0f, ovrProjection_None);
poseState.vrPose.eye = convert(view);
poseState.vrPose.projection = convert(proj);
ovrSessionStatus sessionStatus;
ovr_GetSessionStatus(session, &sessionStatus);
if (sessionStatus.IsVisible) poseState.isVisible = true;
else poseState.isVisible = false;
if (sessionStatus.HmdPresent) poseState.hmdPresenting = true;
else poseState.hmdPresenting = false;
if (sessionStatus.HmdMounted) poseState.hmdMounted = true;
else poseState.hmdMounted = false;
if (sessionStatus.DisplayLost) poseState.displayLost = true;
else poseState.displayLost = false;
if (sessionStatus.ShouldQuit) poseState.shouldQuit = true;
else poseState.shouldQuit = false;
if (sessionStatus.ShouldRecenter) poseState.shouldRecenter = true;
else poseState.shouldRecenter = false;
sensorStates[eye].pose = poseState;
return sensorStates[eye];
}*/
kinc_vr_sensor_state_t kinc_vr_interface_get_sensor_state(int eye) {
kinc_vr_pose_state_t poseState;
ovrQuatf orientation = EyeRenderPose[eye].Orientation;
poseState.vrPose.orientation.x = orientation.x;
poseState.vrPose.orientation.y = orientation.y;
poseState.vrPose.orientation.z = orientation.z;
poseState.vrPose.orientation.w = orientation.w;
ovrVector3f pos = EyeRenderPose[eye].Position;
poseState.vrPose.position.x = pos.x;
poseState.vrPose.position.y = pos.y;
poseState.vrPose.position.z = pos.z;
ovrFovPort fov = hmdDesc.DefaultEyeFov[eye];
poseState.vrPose.left = fov.LeftTan;
poseState.vrPose.right = fov.RightTan;
poseState.vrPose.bottom = fov.DownTan;
poseState.vrPose.top = fov.UpTan;
// Get view and projection matrices
OVR::Matrix4f finalRollPitchYaw = OVR::Matrix4f(EyeRenderPose[eye].Orientation);
OVR::Vector3f finalUp = finalRollPitchYaw.Transform(OVR::Vector3f(0, 1, 0));
OVR::Vector3f finalForward = finalRollPitchYaw.Transform(OVR::Vector3f(0, 0, -1));
OVR::Vector3f shiftedEyePos = EyeRenderPose[eye].Position;
OVR::Matrix4f view = OVR::Matrix4f::LookAtRH(shiftedEyePos, shiftedEyePos + finalForward, finalUp);
OVR::Matrix4f proj = ovrMatrix4f_Projection(hmdDesc.DefaultEyeFov[eye], 0.2f, 1000.0f, ovrProjection_None);
poseState.vrPose.eye = convert(view);
poseState.vrPose.projection = convert(proj);
ovrSessionStatus sessionStatus;
ovr_GetSessionStatus(session, &sessionStatus);
if (sessionStatus.IsVisible)
poseState.isVisible = true;
else
poseState.isVisible = false;
if (sessionStatus.HmdPresent)
poseState.hmdPresenting = true;
else
poseState.hmdPresenting = false;
if (sessionStatus.HmdMounted)
poseState.hmdMounted = true;
else
poseState.hmdMounted = false;
if (sessionStatus.DisplayLost)
poseState.displayLost = true;
else
poseState.displayLost = false;
if (sessionStatus.ShouldQuit)
poseState.shouldQuit = true;
else
poseState.shouldQuit = false;
if (sessionStatus.ShouldRecenter)
poseState.shouldRecenter = true;
else
poseState.shouldRecenter = false;
sensorStates[eye].pose = poseState;
return sensorStates[eye];
}
kinc_vr_pose_state_t kinc_vr_interface_get_controller(int index) {
kinc_vr_pose_state_t todo;
return todo;
}
void kinc_vr_interface_warp_swap() {
// Initialize our single full screen Fov layer.
ovrLayerEyeFov ld;
ld.Header.Type = ovrLayerType_EyeFov;
ld.Header.Flags = ovrLayerFlag_TextureOriginAtBottomLeft; // Because OpenGL.
if (isVisible) {
for (int eye = 0; eye < 2; ++eye) {
ld.ColorTexture[eye] = eyeRenderTexture[eye]->TextureChain;
ld.Viewport[eye] = OVR::Recti(eyeRenderTexture[eye]->GetSize());
ld.Fov[eye] = hmdDesc.DefaultEyeFov[eye];
ld.RenderPose[eye] = EyeRenderPose[eye];
ld.SensorSampleTime = sensorSampleTime;
}
}
ovrLayerHeader *layers = &ld.Header;
ovrResult result = ovr_SubmitFrame(session, frameIndex, nullptr, &layers, 1);
if (!OVR_SUCCESS(result)) {
isVisible = false;
}
else {
isVisible = true;
}
frameIndex++;
// Blit mirror texture to back buffer
glBindFramebuffer(GL_READ_FRAMEBUFFER, mirrorFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
GLint w = Platform.WinSizeW;
GLint h = Platform.WinSizeH;
glBlitFramebuffer(0, h, w, 0, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
}
void kinc_vr_interface_update_tracking_origin(kinc_tracking_origin_t origin) {
switch (origin) {
case KINC_TRACKING_ORIGIN_STAND:
ovr_SetTrackingOriginType(session, ovrTrackingOrigin_FloorLevel);
break;
case KINC_TRACKING_ORIGIN_SIT:
ovr_SetTrackingOriginType(session, ovrTrackingOrigin_EyeLevel);
break;
default:
ovr_SetTrackingOriginType(session, ovrTrackingOrigin_FloorLevel);
break;
}
}
void kinc_vr_interface_reset_hmd_pose() {
ovr_RecenterTrackingOrigin(session);
}
void kinc_vr_interface_ovr_shutdown() {
ovr_Shutdown();
}
#endif