diff --git a/Maekfile.js b/Maekfile.js index 43aea5c..4404a4b 100644 --- a/Maekfile.js +++ b/Maekfile.js @@ -21,7 +21,7 @@ const maek = init_maek(); //====================================================================== const NEST_LIBS = `../nest-libs/${maek.OS}`; -const OPENXR_SDK = `../openxr-sdk-source`; +const OPENXR_SDK = `../openxr-sdk`; //set compile flags (these can also be overridden per-task using the "options" parameter): if (maek.OS === "windows") { @@ -58,7 +58,8 @@ if (maek.OS === "windows") { //linker flags for nest libraries: `-L${NEST_LIBS}/SDL2/lib`, `-lSDL2`, `-lm`, `-ldl`, `-lasound`, `-lpthread`, `-lX11`, `-lXext`, `-lpthread`, `-lrt`, `-lGL`, //the output of sdl-config --static-libs `-L${NEST_LIBS}/libpng/lib`, `-lpng`, - `-L${NEST_LIBS}/zlib/lib`, `-lz` + `-L${NEST_LIBS}/zlib/lib`, `-lz`, + `-L${OPENXR_SDK}/build/linux/src/loader`, `-lopenxr_loader`, ); } else if (maek.OS === "macos") { maek.options.CPPFlags.push( @@ -87,6 +88,9 @@ let copies = [ if (maek.OS === 'windows') { copies.push( maek.COPY(`${NEST_LIBS}/SDL2/dist/SDL2.dll`, `dist/SDL2.dll`) ); } +if (maek.OS === 'linux') { + copies.push( maek.COPY(`${OPENXR_SDK}/build/linux/src/loader/libopenxr_loader.so.1`, `dist/libopenxr_loader.so.1`) ); +} //call rules on the maek object to specify tasks. // rules generally look like: @@ -99,8 +103,9 @@ if (maek.OS === 'windows') { const game_names = [ maek.CPP('PlayMode.cpp'), maek.CPP('main.cpp'), - maek.CPP('LitColorTextureProgram.cpp') + maek.CPP('LitColorTextureProgram.cpp'), //, maek.CPP('ColorTextureProgram.cpp') //not used right now, but you might want it + maek.CPP('XR.cpp') ]; const common_names = [ diff --git a/PlayMode.cpp b/PlayMode.cpp index 1d83844..eaf2804 100644 --- a/PlayMode.cpp +++ b/PlayMode.cpp @@ -1,4 +1,6 @@ #include "PlayMode.hpp" +#include "XR.hpp" +#include "xr_linear.h" //for projection matrix building helper #include "LitColorTextureProgram.hpp" @@ -9,6 +11,7 @@ #include "data_path.hpp" #include +#include #include @@ -168,9 +171,9 @@ void PlayMode::update(float elapsed) { } void PlayMode::draw(glm::uvec2 const &drawable_size) { - //update camera aspect ratio for drawable: + //update window's camera aspect ratio for drawable: camera->aspect = float(drawable_size.x) / float(drawable_size.y); - + //set up light type and position for lit_color_texture_program: // TODO: consider using the Light(s) in the scene to do this glUseProgram(lit_color_texture_program->program); @@ -179,16 +182,8 @@ void PlayMode::draw(glm::uvec2 const &drawable_size) { glUniform3fv(lit_color_texture_program->LIGHT_ENERGY_vec3, 1, glm::value_ptr(glm::vec3(1.0f, 1.0f, 0.95f))); glUseProgram(0); - glClearColor(0.5f, 0.5f, 0.5f, 1.0f); - glClearDepth(1.0f); //1.0 is actually the default value to clear the depth buffer to, but FYI you can change it. - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LESS); //this is the default depth comparison function, but FYI you can change it. - - GL_ERRORS(); //print any errors produced by this setup code - - scene.draw(*camera); + //main scene drawing into the window: + draw_helper(camera->make_projection() * glm::mat4(camera->transform->make_world_to_local())); { //use DrawLines to overlay some text: glDisable(GL_DEPTH_TEST); @@ -211,4 +206,62 @@ void PlayMode::draw(glm::uvec2 const &drawable_size) { glm::vec3(H, 0.0f, 0.0f), glm::vec3(0.0f, H, 0.0f), glm::u8vec4(0xff, 0xff, 0xff, 0x00)); } + + //---------------------------------------------- + + if (xr && xr->next_frame.should_render) { + //set up a transform representing the stage's position in the world: + // NOTE: state's "up" direction is +Y. + Scene::Transform stage; + stage.rotation = glm::quat_cast(glm::mat3( + glm::vec3(1.0f, 0.0f, 0.0f), + glm::vec3(0.0f, 0.0f, 1.0f), + glm::vec3(0.0f,-1.0f, 0.0f) + )); + stage.position = glm::vec3(0.0f, 0.0f, 0.0f); //just center up for now + stage.scale = glm::vec3(10.0f); //let's make the player large! + + for (auto const &view : xr->views) { + if (!view.current_framebuffer) continue; //weird bug but nothing to do, I guess + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, view.current_framebuffer->fb); + glViewport(0, 0, xr->size.x, xr->size.y); + + XrMatrix4x4f xr_proj; + XrMatrix4x4f_CreateProjectionFov(&xr_proj, GRAPHICS_OPENGL, view.fov, 0.1f, 0.0f); + + glm::mat4 proj = glm::mat4( + xr_proj.m[0], xr_proj.m[1], xr_proj.m[2], xr_proj.m[3], + xr_proj.m[4], xr_proj.m[5], xr_proj.m[6], xr_proj.m[7], + xr_proj.m[8], xr_proj.m[9], xr_proj.m[10], xr_proj.m[11], + xr_proj.m[12], xr_proj.m[13], xr_proj.m[14], xr_proj.m[15] + ); + + Scene::Transform at; + at.parent = &stage; + at.position = glm::vec3(view.pose.position.x, view.pose.position.y, view.pose.position.z); + at.rotation = glm::quat(view.pose.orientation.w, view.pose.orientation.x, view.pose.orientation.y, view.pose.orientation.z); + + draw_helper(proj * glm::mat4(at.make_world_to_local())); + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glViewport(0, 0, drawable_size.x, drawable_size.y); + } + } + + +} +void PlayMode::draw_helper(glm::mat4 const &world_to_clip) { + + glClearColor(0.5f, 0.5f, 0.5f, 1.0f); + glClearDepth(1.0f); //1.0 is actually the default value to clear the depth buffer to, but FYI you can change it. + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); //this is the default depth comparison function, but FYI you can change it. + + GL_ERRORS(); //print any errors produced by this setup code + + scene.draw(world_to_clip); + } diff --git a/PlayMode.hpp b/PlayMode.hpp index 1646ed4..4e3bd35 100644 --- a/PlayMode.hpp +++ b/PlayMode.hpp @@ -16,6 +16,9 @@ struct PlayMode : Mode { virtual void update(float elapsed) override; virtual void draw(glm::uvec2 const &drawable_size) override; + //draw_helper called to draw both into game window and into VR views: + void draw_helper(glm::mat4 const &world_to_clip); + //----- game state ----- //input tracking: diff --git a/README.md b/README.md index a63864e..ddb7dc3 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,40 @@ $ cd ovr-openxr-sdk $ unzip ../downloads/ovr_openxr_mobile_sdk_57.0.zip #current version as of this writing ``` +For **desktop** VR use, grab Khronos's OpenXR loader: (see the README.md for more build info; like non-linux build instructions, required packages to install) +``` +$ git clone https://github.com/KhronosGroup/OpenXR-SDK openxr-sdk +$ cd openxr-sdk +$ mkdir -p build/linux +$ cd build/linux +$ cmake -DDYNAMIC_LOADER=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo ../.. +``` + Once all of that is done you should have these three as siblings: ``` 15466-f23-ndk-openxr #this folder android-sdk #android sdk, tools, etc ovr-openxr-sdk #Meta's OpenXR SDK stuff +openxr-sdk #Knrono's OpenXR SDK (optional; for desktop use) +``` + +### SteamVR Linux Notes + +Getting "failed to determine active runtime file path for this environment": + +``` +$ mkdir -p .config/openxr/1 +$ ln -sf ~/.steam/steam/steamapps/common/SteamVR/steamxr_linux64.json ~/.config/openxr/1/active_runtime.json +``` + +Or you can just: +``` +$ XR_RUNTIME_JSON=~/.steam/steam/steamapps/common/SteamVR/steamxr_linux64.json ./dist/game ``` +(As per https://community.khronos.org/t/openxr-loader-how-to-select-runtime/108524) + + +I found that opening SteamVR, then closing SteamVR but leaving steam open resulted in OpenXR calls succeeding. But not having steam open at all or having steamvr open both resulted in CreateInstance failing. ### Hardware @@ -76,5 +104,9 @@ $ ./platform-tools/adb shell ## Building - ... + + +## OpenXR Notes + + diff --git a/XR.cpp b/XR.cpp new file mode 100644 index 0000000..476f1de --- /dev/null +++ b/XR.cpp @@ -0,0 +1,637 @@ +#include "XR.hpp" + +#include "GL.hpp" +#include "gl_errors.hpp" + +#include +#include + +#define XR_USE_GRAPHICS_API_OPENGL +#define XR_USE_PLATFORM_XLIB +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +XR *xr = nullptr; + +XR::XR( + SDL_Window *window, SDL_GLContext context, + std::string const &application_name, + uint32_t application_version, + std::string const &engine_name, + uint32_t engine_version +) { + std::cout << "--- initializing OpenXR ---" << std::endl; + + XrInstanceCreateInfo create_info{XR_TYPE_INSTANCE_CREATE_INFO}; + + //with reference to openxr_program.cpp from openxr-sdk-source + the openxr specification + + //XR_MAX_APPLICATION_NAME_SIZE-1 because std::string.size() doesn't count null bytes + std::memcpy(create_info.applicationInfo.applicationName, application_name.c_str(), + std::min(application_name.size(), size_t(XR_MAX_APPLICATION_NAME_SIZE-1)) ); + + create_info.applicationInfo.applicationVersion = application_version; + + std::memcpy(create_info.applicationInfo.engineName, engine_name.c_str(), + std::min(engine_name.size(), size_t(XR_MAX_ENGINE_NAME_SIZE-1)) ); + + create_info.applicationInfo.engineVersion = engine_version; + + create_info.applicationInfo.apiVersion = XR_CURRENT_API_VERSION; + + //TODO: ON ANROID --> set create_info->next to point to an XrInstanceCreateInfoAndroidKHR structure + // filled in as per openxr-sdk-source's platformplugin_android.cpp + + std::vector< const char * > extensions{ + XR_KHR_OPENGL_ENABLE_EXTENSION_NAME, + //TOOD: ON ANROID --> XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME + }; + + create_info.enabledExtensionCount = uint32_t(extensions.size()); + create_info.enabledExtensionNames = extensions.data(); + + if (XrResult res = xrCreateInstance(&create_info, &instance); + res != XR_SUCCESS) { + instance = XR_NULL_HANDLE; //just in case create broke something + throw std::runtime_error("Failed to create OpenXR instance: " + to_string(res)); + } + + { //Query the instance to learn what runtime this is using: + XrInstanceProperties properties{XR_TYPE_INSTANCE_PROPERTIES}; + if (XrResult res = xrGetInstanceProperties(instance, &properties); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to get OpenXR instance properties: " + to_string(res)); + } + + //Information: + std::cout << "OpenXR Runtime is '" << properties.runtimeName << "', version " + << (properties.runtimeVersion >> 48) + << "." << ((properties.runtimeVersion >> 32) & 0xffff) + << "." << (properties.runtimeVersion & 0xffffffff) + << std::endl; + } + + //----- now select the *system* ----- + + XrSystemGetInfo system_info{XR_TYPE_SYSTEM_GET_INFO}; + + system_info.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; + + XrSystemId system_id{XR_NULL_SYSTEM_ID}; + + if (XrResult res = xrGetSystem(instance, &system_info, &system_id); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to get a system: " + to_string(res)); + } + + { //Query the system to learn a bit more: + XrSystemProperties properties{XR_TYPE_SYSTEM_PROPERTIES}; + + if (XrResult res = xrGetSystemProperties(instance, system_id, &properties); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to get system properties: " + to_string(res)); + } + std::cout << "System is '" << properties.systemName << "' with vendorId " << properties.vendorId << "." << std::endl; + std::cout << " graphics: maxSwapchainImageWidth x Height is " << properties.graphicsProperties.maxSwapchainImageWidth << "x" << properties.graphicsProperties.maxSwapchainImageHeight << ", maxLayerCount is " << properties.graphicsProperties.maxLayerCount << "." << std::endl; + std::cout << " tracking: orientationTracking is " << (properties.trackingProperties.orientationTracking == XR_TRUE ? "true" : "false"); + std::cout << ", positionTracking is " << (properties.trackingProperties.positionTracking == XR_TRUE ? "true" : "false") << "." << std::endl; + + //note that the next field might actually have a long list of structures that might be interesting; depending on (whatever). + } + + //----- graphics requirements ----- + //NOTE: this is required before xrCreateSession(!) + //based on ovr-openxr-sdk's XrApp.cpp + + //TODO: OpenGLES / android code(!) + { //report OpenGL version support + PFN_xrGetOpenGLGraphicsRequirementsKHR xrGetOpenGLGraphicsRequirementsKHR = nullptr; + if (XrResult res = xrGetInstanceProcAddr(instance, "xrGetOpenGLGraphicsRequirementsKHR",(PFN_xrVoidFunction*)(&xrGetOpenGLGraphicsRequirementsKHR)); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to get xrGetOpenGLGraphicsRequrirementsKHR function pointer: " + to_string(res)); + } + XrGraphicsRequirementsOpenGLKHR graphics_requirements = {XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR}; + if (XrResult res = xrGetOpenGLGraphicsRequirementsKHR(instance, system_id, &graphics_requirements); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to get OpenGL graphics requirements: " + to_string(res)); + } + std::cout << "OpenGL requirements:\n"; + std::cout << " min supported version: " << (graphics_requirements.minApiVersionSupported >> 48) << "." << ((graphics_requirements.minApiVersionSupported >> 32) & 0xffff) << "\n"; + std::cout << " max known supported version: " << (graphics_requirements.maxApiVersionSupported >> 48) << "." << ((graphics_requirements.maxApiVersionSupported >> 32) & 0xffff) << " (OpenXR spec says newer may work fine, though)\n"; + std::cout.flush(); + + { //compare to OpenGL's reported version: + GLint major = 0; + GLint minor = 0; + glGetIntegerv(GL_MAJOR_VERSION, &major); + glGetIntegerv(GL_MINOR_VERSION, &minor); + if (XR_MAKE_VERSION(major, minor, 0) < graphics_requirements.minApiVersionSupported) { + std::cerr << "ERROR: reported OpenGL version (" << major << "." << minor << ") is less than the minimum that OpenXR reports as supporting. (Continuing, but don't expect things to work.)" << std::endl; + } + if (XR_MAKE_VERSION(major, minor, 0) > graphics_requirements.maxApiVersionSupported) { + std::cerr << "WARNING: reported OpenGL version (" << major << "." << minor << ") is larger the maximum that OpenXR reports supporting. (Continuing, since this could still be okay.)" << std::endl; + } + } + + //TODO, possibly: *actually* check this somewhere? + } + + { //----- session creation ----- + //getting window manager info as per example in SDL_syswm.h comment: + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + if ( !SDL_GetWindowWMInfo(window, &info) ) { + throw std::runtime_error("Failed to get window manager info from SDL: " + std::string(SDL_GetError())); + } + + //to extract the window's Visual (and, eventually, visualid): + XWindowAttributes window_attributes; + if (Status res = XGetWindowAttributes(info.info.x11.display, info.info.x11.window, &window_attributes); + res == 0) { + throw std::runtime_error("Failed to get window attributes (needed to extract visual)."); + } + + //appears to be the case on SDL's x11 opengl driver: + GLXContext glx_context = reinterpret_cast< GLXContext >(context); + + //How to get the current fbconfig; thanks to a suggestion from: + // https://stackoverflow.com/questions/74104449/getting-the-current-glxfbconfig-in-glx + int fbconfig_id = 0; + if (int ret = glXQueryContext(info.info.x11.display, glx_context, GLX_FBCONFIG_ID, &fbconfig_id); + ret != Success) { + throw std::runtime_error("Failed to query FBConfig id from glX context."); + } + + int fb_configs_size = 0; + int attribs[3] = { GLX_FBCONFIG_ID, fbconfig_id, None }; + GLXFBConfig *fb_configs = glXChooseFBConfig( + info.info.x11.display, + XScreenNumberOfScreen(window_attributes.screen), + attribs, + &fb_configs_size); + + if (fb_configs == NULL || fb_configs_size == 0) { + throw std::runtime_error("Failed to select FBConfig by id."); + } + if (fb_configs_size > 1) { + std::cerr << "WARNING: got more than one config with the id from the current context; using the first, but this may break things." << std::endl; + } + + XrGraphicsBindingOpenGLXlibKHR binding{XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR}; + binding.xDisplay = info.info.x11.display; + binding.visualid = XVisualIDFromVisual(window_attributes.visual); + binding.glxFBConfig = fb_configs[0]; + binding.glxDrawable = glXGetCurrentDrawable(); + binding.glxContext = glx_context; + + + XrSessionCreateInfo create_info{XR_TYPE_SESSION_CREATE_INFO}; + create_info.next = &binding; + create_info.createFlags = 0; //no flags specified yet + create_info.systemId = system_id; + + + if (XrResult res = xrCreateSession(instance, &create_info, &session); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to create session: " + to_string(res)); + } + + XFree(fb_configs); //"use XFree to free the memory returned by glXChooseFBConfig" + } + + { //create stage space: + XrReferenceSpaceCreateInfo create_info{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + create_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; + create_info.poseInReferenceSpace.orientation.w = 1.0f; //everything else can be zero from value-initialization, but orientation needs to be unit-length + + if (XrResult res = xrCreateReferenceSpace(session, &create_info, &stage); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to create stage space: " + to_string(res)); + } + } + + { //look up view information + std::array< XrViewConfigurationView, 2 > views; + views.fill(XrViewConfigurationView{XR_TYPE_VIEW_CONFIGURATION_VIEW}); + const uint32_t views_capacity = uint32_t(views.size()); + uint32_t views_count = 0; + + if (XrResult res = xrEnumerateViewConfigurationViews(instance, system_id, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, views_capacity, &views_count, views.data()); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to get view configuration views."); + } + assert(views_count == 2); //OpenXR spec says it *must* be two views + size.x = views[0].recommendedImageRectWidth; + size.y = views[0].recommendedImageRectHeight; + + std::cout << "Will create images of size " << size.x << "x" << size.y << "." << std::endl; + } + + { //swapchain creation + std::array< int64_t, 64 > formats; + uint32_t format_capacity = uint32_t(formats.size()); + uint32_t format_count = 0; + if (XrResult res = xrEnumerateSwapchainFormats(session, format_capacity, &format_count, formats.data()); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to get enumerate swapchain formats: " + to_string(res)); + } + + bool have_SRGB8 = false; + + std::cout << "Got " << format_count << " swapchain formats." << std::endl; + for (uint32_t f = 0; f < format_count; ++f) { + int64_t format = formats[f]; + if (format == GL_SRGB8) have_SRGB8 = true; + + std::cout << " [" << f << "]: "; + #define DO(fmt) if (format == fmt) { std::cout << #fmt; } else + DO(GL_RGB8) + DO(GL_RGBA8) + DO(GL_SRGB8) + DO(GL_SRGB8_ALPHA8) + DO(GL_RGBA16F) + DO(GL_RGBA32F) + DO(GL_DEPTH_COMPONENT16) + DO(GL_DEPTH_COMPONENT24) + DO(GL_DEPTH_COMPONENT32F) + { std::cout << " as-of-yet untranslated enum 0x" << std::hex << format << std::dec; } + std::cout << std::endl; + } + + if (!have_SRGB8) { + throw std::runtime_error("GL_SRGB8 was not among the preferred formats; this code expects it to be."); + } + + XrSwapchainCreateInfo create_info{XR_TYPE_SWAPCHAIN_CREATE_INFO}; + + create_info.createFlags = 0; + create_info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT; //NOTE: USAGE_SAMPLED_BIT was used by the ovr sdk sample; not sure if this is actually needed + create_info.format = GL_SRGB8; + create_info.sampleCount = 1; + create_info.width = size.x; + create_info.height = size.y; + create_info.faceCount = 1; + create_info.arraySize = 1; + create_info.mipCount = 1; + + for (auto &view : views) { + if (XrResult res = xrCreateSwapchain(session, &create_info, &view.swapchain); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to create swapchain: " + to_string(res)); + } + + uint32_t chain_length = 0; + + //fetch length: + if (XrResult res = xrEnumerateSwapchainImages(view.swapchain, 0, &chain_length, NULL); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to get swapchain length: " + to_string(res)); + } + + std::vector< XrSwapchainImageOpenGLKHR > images(chain_length, XrSwapchainImageOpenGLKHR{XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR}); + //actually fetch image structures: + if (XrResult res = xrEnumerateSwapchainImages(view.swapchain, uint32_t(images.size()), &chain_length, reinterpret_cast< XrSwapchainImageBaseHeader * >(images.data())); + res != XR_SUCCESS) { + throw std::runtime_error("Failed to get swapchain images: " + to_string(res)); + } + assert(chain_length == uint32_t(images.size())); //chain hasn't changed length + + view.framebuffers.resize(images.size()); + for (uint32_t i = 0; i < view.framebuffers.size(); ++i) { + view.framebuffers[i].color_tex = images[i].image; + //set texture sampling state: (ovr sdk sample does this; not sure if it is needed) + glBindTexture(GL_TEXTURE_2D, view.framebuffers[i].color_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); //NEAREST might also be an option here + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glBindTexture(GL_TEXTURE_2D, 0); + + //allocate depth renderbuffer: + glGenRenderbuffers(1, &view.framebuffers[i].depth_rb); + glBindRenderbuffer(GL_RENDERBUFFER, view.framebuffers[i].depth_rb); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, size.x, size.y); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + //allocate framebuffer: + glGenFramebuffers(1, &view.framebuffers[i].fb); + glBindFramebuffer(GL_FRAMEBUFFER, view.framebuffers[i].fb); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, view.framebuffers[i].depth_rb); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view.framebuffers[i].color_tex, 0); + + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + if (status != GL_FRAMEBUFFER_COMPLETE) { + throw std::runtime_error("Failed to create a complete framebuffer " + std::to_string(status)); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + GL_ERRORS(); + } + + std::cout << "view[" << (&view - &views[0]) << "] has " << view.framebuffers.size() << " images in swapchain." << std::endl; + } + } + +} + +XR::~XR() { + std::cout << "--- shutting down OpenXR ---" << std::endl; + + if (running) { + if (XrResult res = xrRequestExitSession(session); + res != XR_SUCCESS) { + std::cerr << "XR failed to xrRequestExitSession: " << to_string(res) << std::endl; + } + //wait a moment for session to stop running + for (uint32_t iter = 0; iter < 10; ++iter) { + poll_events(); + if (!running) break; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + if (running) { + std::cerr << "Waiting a fair bit of time, but session hasn't returned to idle yet." << std::endl; + } + } + + for (auto &view : views) { + for (auto &framebuffer : view.framebuffers) { + if (framebuffer.fb != 0) { + glDeleteFramebuffers(1, &framebuffer.fb); + framebuffer.fb = 0; + } + if (framebuffer.depth_rb != 0) { + glDeleteRenderbuffers(1, &framebuffer.depth_rb); + framebuffer.depth_rb = 0; + } + //don't free color_tex -- xrDestroySwapchain should manage that + } + view.framebuffers.clear(); + + if (view.swapchain != XR_NULL_HANDLE) { + if (XrResult res = xrDestroySwapchain(view.swapchain); + res != XR_SUCCESS) { + std::cerr << "XR failed to destroy swapchain: " << to_string(res) << std::endl; + } + view.swapchain = XR_NULL_HANDLE; + } + } + + if (stage != XR_NULL_HANDLE) { + std::cout << " destroying space..." << std::endl; + if (XrResult res = xrDestroySpace(stage); + res != XR_SUCCESS) { + std::cerr << "XR failed to destroy stage space: " << to_string(res) << std::endl; + } + stage = XR_NULL_HANDLE; + } + + + if (session != XR_NULL_HANDLE) { + std::cout << " destroying session..." << std::endl; + if (XrResult res = xrDestroySession(session); + res != XR_SUCCESS) { + std::cerr << "XR failed to destroy session: " << to_string(res) << std::endl; + } + session = XR_NULL_HANDLE; + } + + //FOR SOME REASON... this hangs on SteamVR on linux. Not sure why. Grumble. + if (instance != XR_NULL_HANDLE) { + std::cout << " destroying instance..." << std::endl; + if (XrResult res = xrDestroyInstance(instance); + res != XR_SUCCESS) { + instance = XR_NULL_HANDLE; + std::cerr << "XR failed to destroy instance: " << to_string(res) << std::endl; + } + instance = XR_NULL_HANDLE; + } +} + +std::string XR::to_string(XrResult result) const { + std::string ret; + bool use_adhoc = true; + + if (instance) { + char buffer[XR_MAX_RESULT_STRING_SIZE]; + XrResult res = xrResultToString(instance, result, buffer); + if (res == XR_SUCCESS) { + ret = buffer; + use_adhoc = false; + } + } + + if (use_adhoc) { + //based on example in openxr_reflection.h: + #define XR_ENUM_CASE_STR(name, val) if (result == name) { ret = #name; } else + #define XR_ENUM_STR(enumType) \ + XR_LIST_ENUM_##enumType(XR_ENUM_CASE_STR) \ + { ret = "Unknown [" + std::to_string(result) + "]"; } + XR_ENUM_STR(XrResult); + #undef XR_ENUM_CASE_STR + #undef XR_ENUM_STR + } + + return ret; +} + + +void XR::poll_events() { + //read events: + while (true) { + XrEventDataBuffer event{XR_TYPE_EVENT_DATA_BUFFER}; + //fill event structure: + if (XrResult res = xrPollEvent(instance, &event); + res == XR_EVENT_UNAVAILABLE) { + break; + } else if (res != XR_SUCCESS) { + std::cerr << "XR failed to poll event: " << to_string(res); + break; + } + //interpret event structure: + if (event.type == XR_TYPE_EVENT_DATA_EVENTS_LOST) { + XrEventDataEventsLost const &event_ = *reinterpret_cast< XrEventDataEventsLost * >(&event); + std::cout << "INFO: Lost " << event_.lostEventCount << " events." << std::endl; + + } else if (event.type == XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING) { + XrEventDataInstanceLossPending const &event_ = *reinterpret_cast< XrEventDataInstanceLossPending * >(&event); + std::cout << "INFO: Instance loss pending at " << event_.lossTime << "." << std::endl; + } else if (event.type == XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED) { + XrEventDataInteractionProfileChanged const &event_ = *reinterpret_cast< XrEventDataInteractionProfileChanged * >(&event); + std::cout << "INFO: Interaction profile changed in " << (event_.session == session ? "this session" : "some other session (?!)") << "." << std::endl; + } else if (event.type == XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING) { + XrEventDataReferenceSpaceChangePending const &event_ = *reinterpret_cast< XrEventDataReferenceSpaceChangePending * >(&event); + std::cout << "INFO: reference space changing at " << event_.changeTime << "." << std::endl; + } else if (event.type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) { + XrEventDataSessionStateChanged const &event_ = *reinterpret_cast< XrEventDataSessionStateChanged * >(&event); + std::cout << "INFO: session state changed at " << event_.time << " to "; + #define XR_ENUM_CASE_STR(name, val) if (event_.state == name) { std::cout << #name; } else + #define XR_ENUM_STR(enumType) \ + XR_LIST_ENUM_##enumType(XR_ENUM_CASE_STR) \ + { std::cout << "unknown [" << std::to_string(event_.state) << "]"; } + XR_ENUM_STR(XrSessionState); + #undef XR_ENUM_CASE_STR + #undef XR_ENUM_STR + std::cout << "." << std::endl; + + session_state = event_.state; + + if (session_state == XR_SESSION_STATE_READY) { + XrSessionBeginInfo begin_info{XR_TYPE_SESSION_BEGIN_INFO}; + begin_info.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + if (XrResult res = xrBeginSession(session, &begin_info); + res != XR_SUCCESS) { + std::cerr << "Error reported beginning session: " << to_string(res) << std::endl; + } + running = true; + } else if (session_state == XR_SESSION_STATE_STOPPING) { + if (XrResult res = xrEndSession(session); + res != XR_SUCCESS) { + std::cerr << "Error reported ending session: " << to_string(res) << std::endl; + } + running = false; + } + } else { + std::cerr << "WARNING: ignoring unrecognized event type " << event.type << "." << std::endl; + } + } +} + +void XR::wait_frame() { + XrFrameState frame_state{XR_TYPE_FRAME_STATE}; + + if (XrResult res = xrWaitFrame(session, NULL /* XrWaitFrameInfo, is empty as of 1.0 */, &frame_state); + res != XR_SUCCESS) { + std::cerr << "Failed to xrWaitFrame: " << to_string(res) << std::endl; + } + next_frame.display_time = frame_state.predictedDisplayTime; + next_frame.should_render = (frame_state.shouldRender == XR_TRUE); +} + +void XR::begin_frame() { + if (XrResult res = xrBeginFrame(session, NULL /* XrBeginFrameInfo, is empty as of 1.0 */); + res != XR_SUCCESS) { + std::cerr << "Failed to xrBeginFrame: " << to_string(res) << std::endl; + } + + //set up current image to render into: + for (auto &view : views) { + //get the index of the next image to render into: + uint32_t index = 0; + if (XrResult res = xrAcquireSwapchainImage(view.swapchain, NULL /* XrSwapchainImageAcquireInfo, empty as of 1.0 */, &index); + res != XR_SUCCESS) { + std::cerr << "Failed to xrAcquireSwapchainImage: " << to_string(res) << std::endl; + } + + //wait for that image to be ready for rendering: + XrSwapchainImageWaitInfo wait_info{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; + wait_info.timeout = XR_INFINITE_DURATION; + if (XrResult res = xrWaitSwapchainImage(view.swapchain, &wait_info); + res != XR_SUCCESS) { + std::cerr << "Failed to xrWaitSwapchainImage: " << to_string(res) << std::endl; + } + + //update the current_framebuffer pointer appropriately: + view.current_framebuffer = &view.framebuffers.at(index); + } + + + //update views with current transform in stage space: + XrViewLocateInfo info{XR_TYPE_VIEW_LOCATE_INFO}; + info.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + info.displayTime = next_frame.display_time; + info.space = stage; + + XrViewState view_state{XR_TYPE_VIEW_STATE}; + std::array< XrView, 2 > located_views; + located_views.fill(XrView{XR_TYPE_VIEW}); + + uint32_t view_capacity = located_views.size(); + uint32_t view_count = 0; + if (XrResult res = xrLocateViews(session, &info, &view_state, view_capacity, &view_count, located_views.data()); + res != XR_SUCCESS) { + std::cerr << "Failed to xrLocateViews: " << to_string(res) << std::endl; + } + + if (view_count != 2) { + std::cerr << "ERROR: Somehow didn't get exactly two views located?!" << std::endl; + return; + } + + for (uint32_t v = 0; v < views.size(); ++v) { + if (view_state.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) { + views[v].pose.orientation = located_views[v].pose.orientation; + } else { + views[v].pose.orientation = XrQuaternionf{0.0f, 0.0f, 0.0f, 1.0f}; + } + if (view_state.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) { + views[v].pose.position = located_views[v].pose.position; + } else { + views[v].pose.position = XrVector3f{0.0f, 0.0f, 0.0f}; + } + views[v].fov = located_views[v].fov; + } + + +} + +void XR::end_frame() { + + //done rendering: release swapchain images + for (auto &view : views) { + if (XrResult res = xrReleaseSwapchainImage(view.swapchain, NULL /* XrSwapchainImageReleaseInfo, empty as of 1.0 */); + res != XR_SUCCESS) { + std::cerr << "Failed to xrReleaseSwapchainImage: " << to_string(res) << std::endl; + } + view.current_framebuffer = nullptr; + } + + //tell compositor about rendered images: + std::array< XrCompositionLayerProjectionView, 2 > projection_views; + projection_views.fill(XrCompositionLayerProjectionView{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}); + + static_assert(std::tuple_size< decltype(views) >::value == std::tuple_size< decltype(projection_views) >::value, "Correct number of eyes."); + + for (uint32_t v = 0; v < projection_views.size(); ++v) { + projection_views[v].pose = views[v].pose; + projection_views[v].fov = views[v].fov; + projection_views[v].subImage.swapchain = views[v].swapchain; + projection_views[v].subImage.imageRect.offset.x = 0; + projection_views[v].subImage.imageRect.offset.y = 0; + projection_views[v].subImage.imageRect.extent.width = size.x; + projection_views[v].subImage.imageRect.extent.height = size.y; + projection_views[v].subImage.imageArrayIndex = 0; + + } + + XrCompositionLayerProjection layer{XR_TYPE_COMPOSITION_LAYER_PROJECTION}; + layer.layerFlags = XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT; //note: "planned for deprecation" + layer.space = stage; + layer.viewCount = projection_views.size(); + layer.views = projection_views.data(); + + std::array< XrCompositionLayerBaseHeader *, 1 > layers; + layers[0] = reinterpret_cast< XrCompositionLayerBaseHeader * >(&layer); + + XrFrameEndInfo info{XR_TYPE_FRAME_END_INFO}; + info.displayTime = next_frame.display_time; + info.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + info.layerCount = layers.size(); + info.layers = layers.data(); + + + if (XrResult res = xrEndFrame(session, &info); + res != XR_SUCCESS) { + std::cerr << "Failed to xrEndFrame: " << to_string(res) << std::endl; + } +} diff --git a/XR.hpp b/XR.hpp new file mode 100644 index 0000000..989897b --- /dev/null +++ b/XR.hpp @@ -0,0 +1,86 @@ +#pragma once + +//XR handles the interface to OpenXR. + +#include "GL.hpp" + +#include +#include + +#include + +#include +#include +#include + +struct XR { + //set up xrInstance: + // throws a std::runtime_error() if initialization fails + //NOTE: must only do with a valid OpenGL (/ OpenGLES) context! + XR( + SDL_Window *window, SDL_GLContext context, + std::string const &application_name, + uint32_t application_version, + std::string const &engine_name = "", + uint32_t engine_version = 0 + ); + + //clean up; destroy xrInstance: + ~XR(); + + //helper for error reporting: + std::string to_string(XrResult result) const; + + //call frequently (e.g., every frame): + void poll_events(); + + //update by poll_events(): + XrSessionState session_state = XR_SESSION_STATE_UNKNOWN; + bool running = false; //is the session running? + + //if running is true, you should call (in this order): + void wait_frame(); //wait for the next frame that needs to be rendered; updates next_frame members: + + struct NextFrameInfo { + int64_t display_time = 0; //nanoseconds; (also a predicted value) + bool should_render = false; + } next_frame; + + void begin_frame(); //indicate that rendering has started (call even if should_render = false; but don't do GPU work); updates views' fov, pose, and current_framebuffer + void end_frame(); //indicate that rendering has finished + + + //------------------ + + //all OpenXR communication happens via an instance: + XrInstance instance{XR_NULL_HANDLE}; + + //actual XR interactions happen within a session: + XrSession session{XR_NULL_HANDLE}; + + //stage space is a space with the origin on the floor in the center of the play area, +Y up, and x and z aligned to the floor: + XrSpace stage{XR_NULL_HANDLE}; + + //sessions need a swapchain for every view they will present: + glm::uvec2 size = glm::uvec2(0); //swapchain image size (same for both eyes) + + struct View { + XrSwapchain swapchain{XR_NULL_HANDLE}; + struct Framebuffer { + GLuint color_tex = 0; //managed by swapchain + GLuint depth_rb = 0; //managed by XR + GLuint fb = 0; //managed by XR + }; + std::vector< Framebuffer > framebuffers; + + //set every frame (in begin_frame()): + const Framebuffer *current_framebuffer = nullptr; //the framebuffer to render into + XrFovf fov; //field of view as four angles from center + XrPosef pose; //pose as orientation (quat) + position (vec3) + }; + + std::array< View, 2 > views; //[0] is left, [1] is right + +}; + +extern XR *xr; //global variable to hold singleton XR instance (created in main) diff --git a/main.cpp b/main.cpp index 9205ff5..0470447 100644 --- a/main.cpp +++ b/main.cpp @@ -20,10 +20,8 @@ #include #endif -//for OpenXR: -#include -#define XR_USE_GRAPHICS_API_OPENGL -#include +//for OpenXR stuff: +#include "XR.hpp" //...and for c++ standard library functions: #include @@ -52,50 +50,6 @@ int main(int argc, char **argv) { try { #endif - //------------ OpenXR init ------------ - - { - //OpenXR needs an "instance" to store global state: - XrInstance instance{XR_NULL_HANDLE}; - XrInstanceCreateInfo create_info{XR_TYPE_INSTANCE_CREATE_INFO}; - - //with reference to openxr_program.cpp from openxr-sdk-source + the openxr specification - - std::strcpy(create_info.applicationInfo.applicationName, "gp23 openxr demo"); - create_info.applicationInfo.apiVersion = XR_CURRENT_API_VERSION; - - //TODO: ON ANROID --> set create_info->next to point to an XrInstanceCreateInfoAndroidKHR structure - // filled in as per openxr-sdk-source's platformplugin_android.cpp - - std::vector< const char * > extensions{ - XR_KHR_OPENGL_ENABLE_EXTENSION_NAME, - //TOOD: ON ANROID --> XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME - }; - - create_info.enabledExtensionCount = uint32_t(extensions.size()); - create_info.enabledExtensionNames = extensions.data(); - - if (XrResult res = xrCreateInstance( &create_info, &instance); - res != XR_SUCCESS) { - std::cerr << "Failed to create OpenXR instance: " << res << std::endl; - return 1; - } - - //Query the instance to learn what runtime this is using: - XrInstanceProperties properties{XR_TYPE_INSTANCE_PROPERTIES}; - if (XrResult res = xrGetInstanceProperties(instance, &properties); - res != XR_SUCCESS) { - std::cerr << "Failed to get OpenXR instance properties: " << res << std::endl; - return 1; - } - std::cout << "OpenXR Runtime is '" << properties.runtimeName << "', version " - << (properties.runtimeVersion >> 48) - << "." << ((properties.runtimeVersion >> 32) & 0xffff) - << "." << (properties.runtimeVersion & 0xffffffff) - << std::endl; - - //TODO, later: xrDestroyInstance ! - } //------------ initialization ------------ @@ -113,7 +67,8 @@ int main(int argc, char **argv) { SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + //4.3 is *MIN SUPPORTED* by SteamVR's OpenXR + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); //create window: @@ -154,9 +109,22 @@ int main(int argc, char **argv) { } } + int non_xr_swap_interval = SDL_GL_GetSwapInterval(); + //Hide mouse cursor (note: showing can be useful for debugging): //SDL_ShowCursor(SDL_DISABLE); + //------------ OpenXR init ------------ + + try { + xr = new XR( + window, context, //needed for SDL / desktop OpenGL mode + "gp23 OpenXR example", 1); //params are application name, application version + } catch (std::runtime_error &e) { + std::cerr << "Failed to initialize OpenXR: " << e.what() << std::endl; + return 1; + } + //------------ load assets -------------- call_load_functions(); @@ -217,11 +185,30 @@ int main(int argc, char **argv) { if (!Mode::current) break; } + //process xr events also (these are things like state changes): + if (xr) xr->poll_events(); + + if (xr && xr->running) { + xr->wait_frame(); //wait for the next frame that needs to be rendered + xr->begin_frame(); //indicate that rendering has started on this frame (NOTE: could *probably* do this later to lower latency if head position isn't important for update) + } + { //(2) call the current mode's "update" function to deal with elapsed time: - auto current_time = std::chrono::high_resolution_clock::now(); - static auto previous_time = current_time; - float elapsed = std::chrono::duration< float >(current_time - previous_time).count(); - previous_time = current_time; + float elapsed; + { //time update from system clock: + auto current_time = std::chrono::high_resolution_clock::now(); + static auto previous_time = current_time; + elapsed = std::chrono::duration< float >(current_time - previous_time).count(); + previous_time = current_time; + } + + //override with time update from OpenXR (if available): + if (xr && xr->running) { + static auto previous_time = xr->next_frame.display_time; + auto current_time = xr->next_frame.display_time; + elapsed = (current_time - previous_time) * 1.0e-9; //nanoseconds -> seconds + previous_time = current_time; + } //if frames are taking a very long time to process, //lag to avoid spiral of death: @@ -232,17 +219,37 @@ int main(int argc, char **argv) { } { //(3) call the current mode's "draw" function to produce output: - + //(note: playmode has extra logic in here to deal with xr's views array) Mode::current->draw(drawable_size); } + if (xr && xr->running) { + xr->end_frame(); + } + //Wait until the recently-drawn frame is shown before doing it all again: - SDL_GL_SwapWindow(window); + if (xr && xr->running) { + if (SDL_GL_GetSwapInterval() != 0) { + //NOTE: in xr mode actually don't wait! + SDL_GL_SetSwapInterval(0); + std::cout << "Note: XR is running, so don't wait on vsync." << std::endl; + } + } else { + if (SDL_GL_GetSwapInterval() != non_xr_swap_interval) { + SDL_GL_SetSwapInterval(non_xr_swap_interval); + std::cout << "Note: XR is not running, switching back to vsync." << std::endl; + } + } + SDL_GL_SwapWindow(window); //NOTE: should probably *not* do this while also trying to render to vr stuff because it will block(!) } - //------------ teardown ------------ + if (xr) { + delete xr; + xr = nullptr; + } + SDL_GL_DeleteContext(context); context = 0; diff --git a/xr_linear.h b/xr_linear.h new file mode 100644 index 0000000..ce65f8d --- /dev/null +++ b/xr_linear.h @@ -0,0 +1,869 @@ +// Copyright (c) 2017-2023, The Khronos Group Inc. +// Copyright (c) 2016, Oculus VR, LLC. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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. +// +// Author: J.M.P. van Waveren +// + +#ifndef XR_LINEAR_H_ +#define XR_LINEAR_H_ + +#include + +/* REUSE-IgnoreStart */ +/* The following has copyright notices that duplicate the header above */ + +/* +================================================================================================ + +Description : Vector, matrix and quaternion math. +Orig. Author : J.M.P. van Waveren +Orig. Date : 12/10/2016 +Language : C99 +Copyright : Copyright (c) 2016 Oculus VR, LLC. All Rights reserved. + + +DESCRIPTION +=========== + +All matrices are column-major. + +INTERFACE +========= + +XrVector2f +XrVector3f +XrVector4f +XrQuaternionf +XrPosef +XrMatrix4x4f + +inline static void XrVector3f_Set(XrVector3f* v, const float value); +inline static void XrVector3f_Add(XrVector3f* result, const XrVector3f* a, const XrVector3f* b); +inline static void XrVector3f_Sub(XrVector3f* result, const XrVector3f* a, const XrVector3f* b); +inline static void XrVector3f_Min(XrVector3f* result, const XrVector3f* a, const XrVector3f* b); +inline static void XrVector3f_Max(XrVector3f* result, const XrVector3f* a, const XrVector3f* b); +inline static void XrVector3f_Decay(XrVector3f* result, const XrVector3f* a, const float value); +inline static void XrVector3f_Lerp(XrVector3f* result, const XrVector3f* a, const XrVector3f* b, const float fraction); +inline static void XrVector3f_Scale(XrVector3f* result, const XrVector3f* a, const float scaleFactor); +inline static void XrVector3f_Normalize(XrVector3f* v); +inline static float XrVector3f_Length(const XrVector3f* v); + +inline static void XrQuaternionf_CreateIdentity(XrQuaternionf* q); +inline static void XrQuaternionf_CreateFromAxisAngle(XrQuaternionf* result, const XrVector3f* axis, const float angleInRadians); +inline static void XrQuaternionf_Lerp(XrQuaternionf* result, const XrQuaternionf* a, const XrQuaternionf* b, const float fraction); +inline static void XrQuaternionf_Multiply(XrQuaternionf* result, const XrQuaternionf* a, const XrQuaternionf* b); +inline static void XrQuaternionf_Invert(XrQuaternionf* result, const XrQuaternionf* q); +inline static void XrQuaternionf_Normalize(XrQuaternionf* q); +inline static void XrQuaternionf_RotateVector3f(XrVector3f* result, const XrQuaternionf* a, const XrVector3f* v); + +inline static void XrPosef_CreateIdentity(XrPosef* result); +inline static void XrPosef_TransformVector3f(XrVector3f* result, const XrPosef* a, const XrVector3f* v); +inline static void XrPosef_Multiply(XrPosef* result, const XrPosef* a, const XrPosef* b); +inline static void XrPosef_Invert(XrPosef* result, const XrPosef* a); + +inline static void XrMatrix4x4f_CreateIdentity(XrMatrix4x4f* result); +inline static void XrMatrix4x4f_CreateTranslation(XrMatrix4x4f* result, const float x, const float y, const float z); +inline static void XrMatrix4x4f_CreateRotation(XrMatrix4x4f* result, const float degreesX, const float degreesY, + const float degreesZ); +inline static void XrMatrix4x4f_CreateScale(XrMatrix4x4f* result, const float x, const float y, const float z); +inline static void XrMatrix4x4f_CreateTranslationRotationScale(XrMatrix4x4f* result, const XrVector3f* translation, + const XrQuaternionf* rotation, const XrVector3f* scale); +inline static void XrMatrix4x4f_CreateFromRigidTransform(XrMatrix4x4f* result, const XrPosef* s); +inline static void XrMatrix4x4f_CreateProjection(XrMatrix4x4f* result, GraphicsAPI graphicsApi, const float tanAngleLeft, + const float tanAngleRight, const float tanAngleUp, float const tanAngleDown, + const float nearZ, const float farZ); +inline static void XrMatrix4x4f_CreateProjectionFov(XrMatrix4x4f* result, GraphicsAPI graphicsApi, const XrFovf fov, + const float nearZ, const float farZ); +inline static void XrMatrix4x4f_CreateFromQuaternion(XrMatrix4x4f* result, const XrQuaternionf* quat); +inline static void XrMatrix4x4f_CreateOffsetScaleForBounds(XrMatrix4x4f* result, const XrMatrix4x4f* matrix, const XrVector3f* mins, + const XrVector3f* maxs); + +inline static bool XrMatrix4x4f_IsAffine(const XrMatrix4x4f* matrix, const float epsilon); +inline static bool XrMatrix4x4f_IsOrthogonal(const XrMatrix4x4f* matrix, const float epsilon); +inline static bool XrMatrix4x4f_IsOrthonormal(const XrMatrix4x4f* matrix, const float epsilon); +inline static bool XrMatrix4x4f_IsRigidBody(const XrMatrix4x4f* matrix, const float epsilon); + +inline static void XrMatrix4x4f_GetTranslation(XrVector3f* result, const XrMatrix4x4f* src); +inline static void XrMatrix4x4f_GetRotation(XrQuaternionf* result, const XrMatrix4x4f* src); +inline static void XrMatrix4x4f_GetScale(XrVector3f* result, const XrMatrix4x4f* src); + +inline static void XrMatrix4x4f_Multiply(XrMatrix4x4f* result, const XrMatrix4x4f* a, const XrMatrix4x4f* b); +inline static void XrMatrix4x4f_Transpose(XrMatrix4x4f* result, const XrMatrix4x4f* src); +inline static void XrMatrix4x4f_Invert(XrMatrix4x4f* result, const XrMatrix4x4f* src); +inline static void XrMatrix4x4f_InvertRigidBody(XrMatrix4x4f* result, const XrMatrix4x4f* src); + +inline static void XrMatrix4x4f_TransformVector3f(XrVector3f* result, const XrMatrix4x4f* m, const XrVector3f* v); +inline static void XrMatrix4x4f_TransformVector4f(XrVector4f* result, const XrMatrix4x4f* m, const XrVector4f* v); + +inline static void XrMatrix4x4f_TransformBounds(XrVector3f* resultMins, XrVector3f* resultMaxs, const XrMatrix4x4f* matrix, + const XrVector3f* mins, const XrVector3f* maxs); +inline static bool XrMatrix4x4f_CullBounds(const XrMatrix4x4f* mvp, const XrVector3f* mins, const XrVector3f* maxs); + +================================================================================================ +*/ + +#include +#include +#include + +#define MATH_PI 3.14159265358979323846f + +#define DEFAULT_NEAR_Z 0.015625f // exact floating point representation +#define INFINITE_FAR_Z 0.0f + +static const XrColor4f XrColorRed = {1.0f, 0.0f, 0.0f, 1.0f}; +static const XrColor4f XrColorGreen = {0.0f, 1.0f, 0.0f, 1.0f}; +static const XrColor4f XrColorBlue = {0.0f, 0.0f, 1.0f, 1.0f}; +static const XrColor4f XrColorYellow = {1.0f, 1.0f, 0.0f, 1.0f}; +static const XrColor4f XrColorPurple = {1.0f, 0.0f, 1.0f, 1.0f}; +static const XrColor4f XrColorCyan = {0.0f, 1.0f, 1.0f, 1.0f}; +static const XrColor4f XrColorLightGrey = {0.7f, 0.7f, 0.7f, 1.0f}; +static const XrColor4f XrColorDarkGrey = {0.3f, 0.3f, 0.3f, 1.0f}; + +typedef enum GraphicsAPI { GRAPHICS_VULKAN, GRAPHICS_OPENGL, GRAPHICS_OPENGL_ES, GRAPHICS_D3D } GraphicsAPI; + +// Column-major, pre-multiplied. This type does not exist in the OpenXR API and is provided for convenience. +typedef struct XrMatrix4x4f { + float m[16]; +} XrMatrix4x4f; + +inline static float XrRcpSqrt(const float x) { + const float SMALLEST_NON_DENORMAL = 1.1754943508222875e-038f; // ( 1U << 23 ) + const float rcp = (x >= SMALLEST_NON_DENORMAL) ? 1.0f / sqrtf(x) : 1.0f; + return rcp; +} + +inline static float XrVector2f_Length(const XrVector2f* v) { return sqrtf(v->x * v->x + v->y * v->y); } + +inline static void XrVector3f_Set(XrVector3f* v, const float value) { + v->x = value; + v->y = value; + v->z = value; +} + +inline static void XrVector3f_Add(XrVector3f* result, const XrVector3f* a, const XrVector3f* b) { + result->x = a->x + b->x; + result->y = a->y + b->y; + result->z = a->z + b->z; +} + +inline static void XrVector3f_Sub(XrVector3f* result, const XrVector3f* a, const XrVector3f* b) { + result->x = a->x - b->x; + result->y = a->y - b->y; + result->z = a->z - b->z; +} + +inline static void XrVector3f_Min(XrVector3f* result, const XrVector3f* a, const XrVector3f* b) { + result->x = (a->x < b->x) ? a->x : b->x; + result->y = (a->y < b->y) ? a->y : b->y; + result->z = (a->z < b->z) ? a->z : b->z; +} + +inline static void XrVector3f_Max(XrVector3f* result, const XrVector3f* a, const XrVector3f* b) { + result->x = (a->x > b->x) ? a->x : b->x; + result->y = (a->y > b->y) ? a->y : b->y; + result->z = (a->z > b->z) ? a->z : b->z; +} + +inline static void XrVector3f_Decay(XrVector3f* result, const XrVector3f* a, const float value) { + result->x = (fabsf(a->x) > value) ? ((a->x > 0.0f) ? (a->x - value) : (a->x + value)) : 0.0f; + result->y = (fabsf(a->y) > value) ? ((a->y > 0.0f) ? (a->y - value) : (a->y + value)) : 0.0f; + result->z = (fabsf(a->z) > value) ? ((a->z > 0.0f) ? (a->z - value) : (a->z + value)) : 0.0f; +} + +inline static void XrVector3f_Lerp(XrVector3f* result, const XrVector3f* a, const XrVector3f* b, const float fraction) { + result->x = a->x + fraction * (b->x - a->x); + result->y = a->y + fraction * (b->y - a->y); + result->z = a->z + fraction * (b->z - a->z); +} + +inline static void XrVector3f_Scale(XrVector3f* result, const XrVector3f* a, const float scaleFactor) { + result->x = a->x * scaleFactor; + result->y = a->y * scaleFactor; + result->z = a->z * scaleFactor; +} + +inline static float XrVector3f_Dot(const XrVector3f* a, const XrVector3f* b) { return a->x * b->x + a->y * b->y + a->z * b->z; } + +// Compute cross product, which generates a normal vector. +// Direction vector can be determined by right-hand rule: Pointing index finder in +// direction a and middle finger in direction b, thumb will point in Cross(a, b). +inline static void XrVector3f_Cross(XrVector3f* result, const XrVector3f* a, const XrVector3f* b) { + result->x = a->y * b->z - a->z * b->y; + result->y = a->z * b->x - a->x * b->z; + result->z = a->x * b->y - a->y * b->x; +} + +inline static void XrVector3f_Normalize(XrVector3f* v) { + const float lengthRcp = XrRcpSqrt(v->x * v->x + v->y * v->y + v->z * v->z); + v->x *= lengthRcp; + v->y *= lengthRcp; + v->z *= lengthRcp; +} + +inline static float XrVector3f_Length(const XrVector3f* v) { return sqrtf(v->x * v->x + v->y * v->y + v->z * v->z); } + +inline static void XrQuaternionf_CreateIdentity(XrQuaternionf* q) { + q->x = 0.0f; + q->y = 0.0f; + q->z = 0.0f; + q->w = 1.0f; +} + +inline static void XrQuaternionf_CreateFromAxisAngle(XrQuaternionf* result, const XrVector3f* axis, const float angleInRadians) { + float s = sinf(angleInRadians / 2.0f); + float lengthRcp = XrRcpSqrt(axis->x * axis->x + axis->y * axis->y + axis->z * axis->z); + result->x = s * axis->x * lengthRcp; + result->y = s * axis->y * lengthRcp; + result->z = s * axis->z * lengthRcp; + result->w = cosf(angleInRadians / 2.0f); +} + +inline static void XrQuaternionf_Lerp(XrQuaternionf* result, const XrQuaternionf* a, const XrQuaternionf* b, const float fraction) { + const float s = a->x * b->x + a->y * b->y + a->z * b->z + a->w * b->w; + const float fa = 1.0f - fraction; + const float fb = (s < 0.0f) ? -fraction : fraction; + const float x = a->x * fa + b->x * fb; + const float y = a->y * fa + b->y * fb; + const float z = a->z * fa + b->z * fb; + const float w = a->w * fa + b->w * fb; + const float lengthRcp = XrRcpSqrt(x * x + y * y + z * z + w * w); + result->x = x * lengthRcp; + result->y = y * lengthRcp; + result->z = z * lengthRcp; + result->w = w * lengthRcp; +} + +inline static void XrQuaternionf_Multiply(XrQuaternionf* result, const XrQuaternionf* a, const XrQuaternionf* b) { + result->x = (b->w * a->x) + (b->x * a->w) + (b->y * a->z) - (b->z * a->y); + result->y = (b->w * a->y) - (b->x * a->z) + (b->y * a->w) + (b->z * a->x); + result->z = (b->w * a->z) + (b->x * a->y) - (b->y * a->x) + (b->z * a->w); + result->w = (b->w * a->w) - (b->x * a->x) - (b->y * a->y) - (b->z * a->z); +} + +inline static void XrQuaternionf_Invert(XrQuaternionf* result, const XrQuaternionf* q) { + result->x = -q->x; + result->y = -q->y; + result->z = -q->z; + result->w = q->w; +} + +inline static void XrQuaternionf_Normalize(XrQuaternionf* q) { + const float lengthRcp = XrRcpSqrt(q->x * q->x + q->y * q->y + q->z * q->z + q->w * q->w); + q->x *= lengthRcp; + q->y *= lengthRcp; + q->z *= lengthRcp; + q->w *= lengthRcp; +} + +inline static void XrQuaternionf_RotateVector3f(XrVector3f* result, const XrQuaternionf* a, const XrVector3f* v) { + XrQuaternionf q = {v->x, v->y, v->z, 0.0f}; + XrQuaternionf aq; + XrQuaternionf_Multiply(&aq, &q, a); + XrQuaternionf aInv; + XrQuaternionf_Invert(&aInv, a); + XrQuaternionf aqaInv; + XrQuaternionf_Multiply(&aqaInv, &aInv, &aq); + + result->x = aqaInv.x; + result->y = aqaInv.y; + result->z = aqaInv.z; +} + +inline static void XrPosef_CreateIdentity(XrPosef* result) { + XrQuaternionf_CreateIdentity(&result->orientation); + XrVector3f_Set(&result->position, 0); +} + +inline static void XrPosef_TransformVector3f(XrVector3f* result, const XrPosef* a, const XrVector3f* v) { + XrVector3f r0; + XrQuaternionf_RotateVector3f(&r0, &a->orientation, v); + XrVector3f_Add(result, &r0, &a->position); +} + +inline static void XrPosef_Multiply(XrPosef* result, const XrPosef* a, const XrPosef* b) { + XrQuaternionf_Multiply(&result->orientation, &b->orientation, &a->orientation); + XrPosef_TransformVector3f(&result->position, a, &b->position); +} + +inline static void XrPosef_Invert(XrPosef* result, const XrPosef* a) { + XrQuaternionf_Invert(&result->orientation, &a->orientation); + XrVector3f aPosNeg; + XrVector3f_Scale(&aPosNeg, &a->position, -1.0f); + XrQuaternionf_RotateVector3f(&result->position, &result->orientation, &aPosNeg); +} + +// Use left-multiplication to accumulate transformations. +inline static void XrMatrix4x4f_Multiply(XrMatrix4x4f* result, const XrMatrix4x4f* a, const XrMatrix4x4f* b) { + result->m[0] = a->m[0] * b->m[0] + a->m[4] * b->m[1] + a->m[8] * b->m[2] + a->m[12] * b->m[3]; + result->m[1] = a->m[1] * b->m[0] + a->m[5] * b->m[1] + a->m[9] * b->m[2] + a->m[13] * b->m[3]; + result->m[2] = a->m[2] * b->m[0] + a->m[6] * b->m[1] + a->m[10] * b->m[2] + a->m[14] * b->m[3]; + result->m[3] = a->m[3] * b->m[0] + a->m[7] * b->m[1] + a->m[11] * b->m[2] + a->m[15] * b->m[3]; + + result->m[4] = a->m[0] * b->m[4] + a->m[4] * b->m[5] + a->m[8] * b->m[6] + a->m[12] * b->m[7]; + result->m[5] = a->m[1] * b->m[4] + a->m[5] * b->m[5] + a->m[9] * b->m[6] + a->m[13] * b->m[7]; + result->m[6] = a->m[2] * b->m[4] + a->m[6] * b->m[5] + a->m[10] * b->m[6] + a->m[14] * b->m[7]; + result->m[7] = a->m[3] * b->m[4] + a->m[7] * b->m[5] + a->m[11] * b->m[6] + a->m[15] * b->m[7]; + + result->m[8] = a->m[0] * b->m[8] + a->m[4] * b->m[9] + a->m[8] * b->m[10] + a->m[12] * b->m[11]; + result->m[9] = a->m[1] * b->m[8] + a->m[5] * b->m[9] + a->m[9] * b->m[10] + a->m[13] * b->m[11]; + result->m[10] = a->m[2] * b->m[8] + a->m[6] * b->m[9] + a->m[10] * b->m[10] + a->m[14] * b->m[11]; + result->m[11] = a->m[3] * b->m[8] + a->m[7] * b->m[9] + a->m[11] * b->m[10] + a->m[15] * b->m[11]; + + result->m[12] = a->m[0] * b->m[12] + a->m[4] * b->m[13] + a->m[8] * b->m[14] + a->m[12] * b->m[15]; + result->m[13] = a->m[1] * b->m[12] + a->m[5] * b->m[13] + a->m[9] * b->m[14] + a->m[13] * b->m[15]; + result->m[14] = a->m[2] * b->m[12] + a->m[6] * b->m[13] + a->m[10] * b->m[14] + a->m[14] * b->m[15]; + result->m[15] = a->m[3] * b->m[12] + a->m[7] * b->m[13] + a->m[11] * b->m[14] + a->m[15] * b->m[15]; +} + +// Creates the transpose of the given matrix. +inline static void XrMatrix4x4f_Transpose(XrMatrix4x4f* result, const XrMatrix4x4f* src) { + result->m[0] = src->m[0]; + result->m[1] = src->m[4]; + result->m[2] = src->m[8]; + result->m[3] = src->m[12]; + + result->m[4] = src->m[1]; + result->m[5] = src->m[5]; + result->m[6] = src->m[9]; + result->m[7] = src->m[13]; + + result->m[8] = src->m[2]; + result->m[9] = src->m[6]; + result->m[10] = src->m[10]; + result->m[11] = src->m[14]; + + result->m[12] = src->m[3]; + result->m[13] = src->m[7]; + result->m[14] = src->m[11]; + result->m[15] = src->m[15]; +} + +// Returns a 3x3 minor of a 4x4 matrix. +inline static float XrMatrix4x4f_Minor(const XrMatrix4x4f* matrix, int r0, int r1, int r2, int c0, int c1, int c2) { + return matrix->m[4 * r0 + c0] * + (matrix->m[4 * r1 + c1] * matrix->m[4 * r2 + c2] - matrix->m[4 * r2 + c1] * matrix->m[4 * r1 + c2]) - + matrix->m[4 * r0 + c1] * + (matrix->m[4 * r1 + c0] * matrix->m[4 * r2 + c2] - matrix->m[4 * r2 + c0] * matrix->m[4 * r1 + c2]) + + matrix->m[4 * r0 + c2] * + (matrix->m[4 * r1 + c0] * matrix->m[4 * r2 + c1] - matrix->m[4 * r2 + c0] * matrix->m[4 * r1 + c1]); +} + +// Calculates the inverse of a 4x4 matrix. +inline static void XrMatrix4x4f_Invert(XrMatrix4x4f* result, const XrMatrix4x4f* src) { + const float rcpDet = + 1.0f / (src->m[0] * XrMatrix4x4f_Minor(src, 1, 2, 3, 1, 2, 3) - src->m[1] * XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 2, 3) + + src->m[2] * XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 1, 3) - src->m[3] * XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 1, 2)); + + result->m[0] = XrMatrix4x4f_Minor(src, 1, 2, 3, 1, 2, 3) * rcpDet; + result->m[1] = -XrMatrix4x4f_Minor(src, 0, 2, 3, 1, 2, 3) * rcpDet; + result->m[2] = XrMatrix4x4f_Minor(src, 0, 1, 3, 1, 2, 3) * rcpDet; + result->m[3] = -XrMatrix4x4f_Minor(src, 0, 1, 2, 1, 2, 3) * rcpDet; + result->m[4] = -XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 2, 3) * rcpDet; + result->m[5] = XrMatrix4x4f_Minor(src, 0, 2, 3, 0, 2, 3) * rcpDet; + result->m[6] = -XrMatrix4x4f_Minor(src, 0, 1, 3, 0, 2, 3) * rcpDet; + result->m[7] = XrMatrix4x4f_Minor(src, 0, 1, 2, 0, 2, 3) * rcpDet; + result->m[8] = XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 1, 3) * rcpDet; + result->m[9] = -XrMatrix4x4f_Minor(src, 0, 2, 3, 0, 1, 3) * rcpDet; + result->m[10] = XrMatrix4x4f_Minor(src, 0, 1, 3, 0, 1, 3) * rcpDet; + result->m[11] = -XrMatrix4x4f_Minor(src, 0, 1, 2, 0, 1, 3) * rcpDet; + result->m[12] = -XrMatrix4x4f_Minor(src, 1, 2, 3, 0, 1, 2) * rcpDet; + result->m[13] = XrMatrix4x4f_Minor(src, 0, 2, 3, 0, 1, 2) * rcpDet; + result->m[14] = -XrMatrix4x4f_Minor(src, 0, 1, 3, 0, 1, 2) * rcpDet; + result->m[15] = XrMatrix4x4f_Minor(src, 0, 1, 2, 0, 1, 2) * rcpDet; +} + +// Calculates the inverse of a rigid body transform. +inline static void XrMatrix4x4f_InvertRigidBody(XrMatrix4x4f* result, const XrMatrix4x4f* src) { + result->m[0] = src->m[0]; + result->m[1] = src->m[4]; + result->m[2] = src->m[8]; + result->m[3] = 0.0f; + result->m[4] = src->m[1]; + result->m[5] = src->m[5]; + result->m[6] = src->m[9]; + result->m[7] = 0.0f; + result->m[8] = src->m[2]; + result->m[9] = src->m[6]; + result->m[10] = src->m[10]; + result->m[11] = 0.0f; + result->m[12] = -(src->m[0] * src->m[12] + src->m[1] * src->m[13] + src->m[2] * src->m[14]); + result->m[13] = -(src->m[4] * src->m[12] + src->m[5] * src->m[13] + src->m[6] * src->m[14]); + result->m[14] = -(src->m[8] * src->m[12] + src->m[9] * src->m[13] + src->m[10] * src->m[14]); + result->m[15] = 1.0f; +} + +// Creates an identity matrix. +inline static void XrMatrix4x4f_CreateIdentity(XrMatrix4x4f* result) { + result->m[0] = 1.0f; + result->m[1] = 0.0f; + result->m[2] = 0.0f; + result->m[3] = 0.0f; + result->m[4] = 0.0f; + result->m[5] = 1.0f; + result->m[6] = 0.0f; + result->m[7] = 0.0f; + result->m[8] = 0.0f; + result->m[9] = 0.0f; + result->m[10] = 1.0f; + result->m[11] = 0.0f; + result->m[12] = 0.0f; + result->m[13] = 0.0f; + result->m[14] = 0.0f; + result->m[15] = 1.0f; +} + +// Creates a translation matrix. +inline static void XrMatrix4x4f_CreateTranslation(XrMatrix4x4f* result, const float x, const float y, const float z) { + result->m[0] = 1.0f; + result->m[1] = 0.0f; + result->m[2] = 0.0f; + result->m[3] = 0.0f; + result->m[4] = 0.0f; + result->m[5] = 1.0f; + result->m[6] = 0.0f; + result->m[7] = 0.0f; + result->m[8] = 0.0f; + result->m[9] = 0.0f; + result->m[10] = 1.0f; + result->m[11] = 0.0f; + result->m[12] = x; + result->m[13] = y; + result->m[14] = z; + result->m[15] = 1.0f; +} + +// Creates a rotation matrix. +// If -Z=forward, +Y=up, +X=right, then radiansX=pitch, radiansY=yaw, radiansZ=roll. +inline static void XrMatrix4x4f_CreateRotationRadians(XrMatrix4x4f* result, const float radiansX, const float radiansY, + const float radiansZ) { + const float sinX = sinf(radiansX); + const float cosX = cosf(radiansX); + const XrMatrix4x4f rotationX = {{1, 0, 0, 0, 0, cosX, sinX, 0, 0, -sinX, cosX, 0, 0, 0, 0, 1}}; + const float sinY = sinf(radiansY); + const float cosY = cosf(radiansY); + const XrMatrix4x4f rotationY = {{cosY, 0, -sinY, 0, 0, 1, 0, 0, sinY, 0, cosY, 0, 0, 0, 0, 1}}; + const float sinZ = sinf(radiansZ); + const float cosZ = cosf(radiansZ); + const XrMatrix4x4f rotationZ = {{cosZ, sinZ, 0, 0, -sinZ, cosZ, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}}; + XrMatrix4x4f rotationXY; + XrMatrix4x4f_Multiply(&rotationXY, &rotationY, &rotationX); + XrMatrix4x4f_Multiply(result, &rotationZ, &rotationXY); +} + +// Creates a rotation matrix. +// If -Z=forward, +Y=up, +X=right, then degreesX=pitch, degreesY=yaw, degreesZ=roll. +inline static void XrMatrix4x4f_CreateRotation(XrMatrix4x4f* result, const float degreesX, const float degreesY, + const float degreesZ) { + XrMatrix4x4f_CreateRotationRadians(result, degreesX * (MATH_PI / 180.0f), degreesY * (MATH_PI / 180.0f), + degreesZ * (MATH_PI / 180.0f)); +} + +// Creates a scale matrix. +inline static void XrMatrix4x4f_CreateScale(XrMatrix4x4f* result, const float x, const float y, const float z) { + result->m[0] = x; + result->m[1] = 0.0f; + result->m[2] = 0.0f; + result->m[3] = 0.0f; + result->m[4] = 0.0f; + result->m[5] = y; + result->m[6] = 0.0f; + result->m[7] = 0.0f; + result->m[8] = 0.0f; + result->m[9] = 0.0f; + result->m[10] = z; + result->m[11] = 0.0f; + result->m[12] = 0.0f; + result->m[13] = 0.0f; + result->m[14] = 0.0f; + result->m[15] = 1.0f; +} + +// Creates a matrix from a quaternion. +inline static void XrMatrix4x4f_CreateFromQuaternion(XrMatrix4x4f* result, const XrQuaternionf* quat) { + const float x2 = quat->x + quat->x; + const float y2 = quat->y + quat->y; + const float z2 = quat->z + quat->z; + + const float xx2 = quat->x * x2; + const float yy2 = quat->y * y2; + const float zz2 = quat->z * z2; + + const float yz2 = quat->y * z2; + const float wx2 = quat->w * x2; + const float xy2 = quat->x * y2; + const float wz2 = quat->w * z2; + const float xz2 = quat->x * z2; + const float wy2 = quat->w * y2; + + result->m[0] = 1.0f - yy2 - zz2; + result->m[1] = xy2 + wz2; + result->m[2] = xz2 - wy2; + result->m[3] = 0.0f; + + result->m[4] = xy2 - wz2; + result->m[5] = 1.0f - xx2 - zz2; + result->m[6] = yz2 + wx2; + result->m[7] = 0.0f; + + result->m[8] = xz2 + wy2; + result->m[9] = yz2 - wx2; + result->m[10] = 1.0f - xx2 - yy2; + result->m[11] = 0.0f; + + result->m[12] = 0.0f; + result->m[13] = 0.0f; + result->m[14] = 0.0f; + result->m[15] = 1.0f; +} + +// Creates a combined translation(rotation(scale(object))) matrix. +inline static void XrMatrix4x4f_CreateTranslationRotationScale(XrMatrix4x4f* result, const XrVector3f* translation, + const XrQuaternionf* rotation, const XrVector3f* scale) { + XrMatrix4x4f scaleMatrix; + XrMatrix4x4f_CreateScale(&scaleMatrix, scale->x, scale->y, scale->z); + + XrMatrix4x4f rotationMatrix; + XrMatrix4x4f_CreateFromQuaternion(&rotationMatrix, rotation); + + XrMatrix4x4f translationMatrix; + XrMatrix4x4f_CreateTranslation(&translationMatrix, translation->x, translation->y, translation->z); + + XrMatrix4x4f combinedMatrix; + XrMatrix4x4f_Multiply(&combinedMatrix, &rotationMatrix, &scaleMatrix); + XrMatrix4x4f_Multiply(result, &translationMatrix, &combinedMatrix); +} + +inline static void XrMatrix4x4f_CreateFromRigidTransform(XrMatrix4x4f* result, const XrPosef* s) { + const XrVector3f identityScale = {1.0f, 1.0f, 1.0f}; + XrMatrix4x4f_CreateTranslationRotationScale(result, &s->position, &s->orientation, &identityScale); +} + +// Creates a projection matrix based on the specified dimensions. +// The projection matrix transforms -Z=forward, +Y=up, +X=right to the appropriate clip space for the graphics API. +// The far plane is placed at infinity if farZ <= nearZ. +// An infinite projection matrix is preferred for rasterization because, except for +// things *right* up against the near plane, it always provides better precision: +// "Tightening the Precision of Perspective Rendering" +// Paul Upchurch, Mathieu Desbrun +// Journal of Graphics Tools, Volume 16, Issue 1, 2012 +inline static void XrMatrix4x4f_CreateProjection(XrMatrix4x4f* result, GraphicsAPI graphicsApi, const float tanAngleLeft, + const float tanAngleRight, const float tanAngleUp, float const tanAngleDown, + const float nearZ, const float farZ) { + const float tanAngleWidth = tanAngleRight - tanAngleLeft; + + // Set to tanAngleDown - tanAngleUp for a clip space with positive Y down (Vulkan). + // Set to tanAngleUp - tanAngleDown for a clip space with positive Y up (OpenGL / D3D / Metal). + const float tanAngleHeight = graphicsApi == GRAPHICS_VULKAN ? (tanAngleDown - tanAngleUp) : (tanAngleUp - tanAngleDown); + + // Set to nearZ for a [-1,1] Z clip space (OpenGL / OpenGL ES). + // Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal). + const float offsetZ = (graphicsApi == GRAPHICS_OPENGL || graphicsApi == GRAPHICS_OPENGL_ES) ? nearZ : 0; + + if (farZ <= nearZ) { + // place the far plane at infinity + result->m[0] = 2.0f / tanAngleWidth; + result->m[4] = 0.0f; + result->m[8] = (tanAngleRight + tanAngleLeft) / tanAngleWidth; + result->m[12] = 0.0f; + + result->m[1] = 0.0f; + result->m[5] = 2.0f / tanAngleHeight; + result->m[9] = (tanAngleUp + tanAngleDown) / tanAngleHeight; + result->m[13] = 0.0f; + + result->m[2] = 0.0f; + result->m[6] = 0.0f; + result->m[10] = -1.0f; + result->m[14] = -(nearZ + offsetZ); + + result->m[3] = 0.0f; + result->m[7] = 0.0f; + result->m[11] = -1.0f; + result->m[15] = 0.0f; + } else { + // normal projection + result->m[0] = 2.0f / tanAngleWidth; + result->m[4] = 0.0f; + result->m[8] = (tanAngleRight + tanAngleLeft) / tanAngleWidth; + result->m[12] = 0.0f; + + result->m[1] = 0.0f; + result->m[5] = 2.0f / tanAngleHeight; + result->m[9] = (tanAngleUp + tanAngleDown) / tanAngleHeight; + result->m[13] = 0.0f; + + result->m[2] = 0.0f; + result->m[6] = 0.0f; + result->m[10] = -(farZ + offsetZ) / (farZ - nearZ); + result->m[14] = -(farZ * (nearZ + offsetZ)) / (farZ - nearZ); + + result->m[3] = 0.0f; + result->m[7] = 0.0f; + result->m[11] = -1.0f; + result->m[15] = 0.0f; + } +} + +// Creates a projection matrix based on the specified FOV. +inline static void XrMatrix4x4f_CreateProjectionFov(XrMatrix4x4f* result, GraphicsAPI graphicsApi, const XrFovf fov, + const float nearZ, const float farZ) { + const float tanLeft = tanf(fov.angleLeft); + const float tanRight = tanf(fov.angleRight); + + const float tanDown = tanf(fov.angleDown); + const float tanUp = tanf(fov.angleUp); + + XrMatrix4x4f_CreateProjection(result, graphicsApi, tanLeft, tanRight, tanUp, tanDown, nearZ, farZ); +} + +// Creates a matrix that transforms the -1 to 1 cube to cover the given 'mins' and 'maxs' transformed with the given 'matrix'. +inline static void XrMatrix4x4f_CreateOffsetScaleForBounds(XrMatrix4x4f* result, const XrMatrix4x4f* matrix, const XrVector3f* mins, + const XrVector3f* maxs) { + const XrVector3f offset = {(maxs->x + mins->x) * 0.5f, (maxs->y + mins->y) * 0.5f, (maxs->z + mins->z) * 0.5f}; + const XrVector3f scale = {(maxs->x - mins->x) * 0.5f, (maxs->y - mins->y) * 0.5f, (maxs->z - mins->z) * 0.5f}; + + result->m[0] = matrix->m[0] * scale.x; + result->m[1] = matrix->m[1] * scale.x; + result->m[2] = matrix->m[2] * scale.x; + result->m[3] = matrix->m[3] * scale.x; + + result->m[4] = matrix->m[4] * scale.y; + result->m[5] = matrix->m[5] * scale.y; + result->m[6] = matrix->m[6] * scale.y; + result->m[7] = matrix->m[7] * scale.y; + + result->m[8] = matrix->m[8] * scale.z; + result->m[9] = matrix->m[9] * scale.z; + result->m[10] = matrix->m[10] * scale.z; + result->m[11] = matrix->m[11] * scale.z; + + result->m[12] = matrix->m[12] + matrix->m[0] * offset.x + matrix->m[4] * offset.y + matrix->m[8] * offset.z; + result->m[13] = matrix->m[13] + matrix->m[1] * offset.x + matrix->m[5] * offset.y + matrix->m[9] * offset.z; + result->m[14] = matrix->m[14] + matrix->m[2] * offset.x + matrix->m[6] * offset.y + matrix->m[10] * offset.z; + result->m[15] = matrix->m[15] + matrix->m[3] * offset.x + matrix->m[7] * offset.y + matrix->m[11] * offset.z; +} + +// Returns true if the given matrix is affine. +inline static bool XrMatrix4x4f_IsAffine(const XrMatrix4x4f* matrix, const float epsilon) { + return fabsf(matrix->m[3]) <= epsilon && fabsf(matrix->m[7]) <= epsilon && fabsf(matrix->m[11]) <= epsilon && + fabsf(matrix->m[15] - 1.0f) <= epsilon; +} + +// Returns true if the given matrix is orthogonal. +inline static bool XrMatrix4x4f_IsOrthogonal(const XrMatrix4x4f* matrix, const float epsilon) { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (i != j) { + if (fabsf(matrix->m[4 * i + 0] * matrix->m[4 * j + 0] + matrix->m[4 * i + 1] * matrix->m[4 * j + 1] + + matrix->m[4 * i + 2] * matrix->m[4 * j + 2]) > epsilon) { + return false; + } + if (fabsf(matrix->m[4 * 0 + i] * matrix->m[4 * 0 + j] + matrix->m[4 * 1 + i] * matrix->m[4 * 1 + j] + + matrix->m[4 * 2 + i] * matrix->m[4 * 2 + j]) > epsilon) { + return false; + } + } + } + } + return true; +} + +// Returns true if the given matrix is orthonormal. +inline static bool XrMatrix4x4f_IsOrthonormal(const XrMatrix4x4f* matrix, const float epsilon) { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + const float kd = (i == j) ? 1.0f : 0.0f; // Kronecker delta + if (fabsf(kd - (matrix->m[4 * i + 0] * matrix->m[4 * j + 0] + matrix->m[4 * i + 1] * matrix->m[4 * j + 1] + + matrix->m[4 * i + 2] * matrix->m[4 * j + 2])) > epsilon) { + return false; + } + if (fabsf(kd - (matrix->m[4 * 0 + i] * matrix->m[4 * 0 + j] + matrix->m[4 * 1 + i] * matrix->m[4 * 1 + j] + + matrix->m[4 * 2 + i] * matrix->m[4 * 2 + j])) > epsilon) { + return false; + } + } + } + return true; +} + +// Returns true if the given matrix is a rigid body transform. +inline static bool XrMatrix4x4f_IsRigidBody(const XrMatrix4x4f* matrix, const float epsilon) { + return XrMatrix4x4f_IsAffine(matrix, epsilon) && XrMatrix4x4f_IsOrthonormal(matrix, epsilon); +} + +// Get the translation from a combined translation(rotation(scale(object))) matrix. +inline static void XrMatrix4x4f_GetTranslation(XrVector3f* result, const XrMatrix4x4f* src) { + assert(XrMatrix4x4f_IsAffine(src, 1e-4f)); + assert(XrMatrix4x4f_IsOrthogonal(src, 1e-4f)); + + result->x = src->m[12]; + result->y = src->m[13]; + result->z = src->m[14]; +} + +// Get the rotation from a combined translation(rotation(scale(object))) matrix. +inline static void XrMatrix4x4f_GetRotation(XrQuaternionf* result, const XrMatrix4x4f* src) { + assert(XrMatrix4x4f_IsAffine(src, 1e-4f)); + assert(XrMatrix4x4f_IsOrthogonal(src, 1e-4f)); + + const float rcpScaleX = XrRcpSqrt(src->m[0] * src->m[0] + src->m[1] * src->m[1] + src->m[2] * src->m[2]); + const float rcpScaleY = XrRcpSqrt(src->m[4] * src->m[4] + src->m[5] * src->m[5] + src->m[6] * src->m[6]); + const float rcpScaleZ = XrRcpSqrt(src->m[8] * src->m[8] + src->m[9] * src->m[9] + src->m[10] * src->m[10]); + const float m[9] = {src->m[0] * rcpScaleX, src->m[1] * rcpScaleX, src->m[2] * rcpScaleX, + src->m[4] * rcpScaleY, src->m[5] * rcpScaleY, src->m[6] * rcpScaleY, + src->m[8] * rcpScaleZ, src->m[9] * rcpScaleZ, src->m[10] * rcpScaleZ}; + if (m[0 * 3 + 0] + m[1 * 3 + 1] + m[2 * 3 + 2] > 0.0f) { + float t = +m[0 * 3 + 0] + m[1 * 3 + 1] + m[2 * 3 + 2] + 1.0f; + float s = XrRcpSqrt(t) * 0.5f; + result->w = s * t; + result->z = (m[0 * 3 + 1] - m[1 * 3 + 0]) * s; + result->y = (m[2 * 3 + 0] - m[0 * 3 + 2]) * s; + result->x = (m[1 * 3 + 2] - m[2 * 3 + 1]) * s; + } else if (m[0 * 3 + 0] > m[1 * 3 + 1] && m[0 * 3 + 0] > m[2 * 3 + 2]) { + float t = +m[0 * 3 + 0] - m[1 * 3 + 1] - m[2 * 3 + 2] + 1.0f; + float s = XrRcpSqrt(t) * 0.5f; + result->x = s * t; + result->y = (m[0 * 3 + 1] + m[1 * 3 + 0]) * s; + result->z = (m[2 * 3 + 0] + m[0 * 3 + 2]) * s; + result->w = (m[1 * 3 + 2] - m[2 * 3 + 1]) * s; + } else if (m[1 * 3 + 1] > m[2 * 3 + 2]) { + float t = -m[0 * 3 + 0] + m[1 * 3 + 1] - m[2 * 3 + 2] + 1.0f; + float s = XrRcpSqrt(t) * 0.5f; + result->y = s * t; + result->x = (m[0 * 3 + 1] + m[1 * 3 + 0]) * s; + result->w = (m[2 * 3 + 0] - m[0 * 3 + 2]) * s; + result->z = (m[1 * 3 + 2] + m[2 * 3 + 1]) * s; + } else { + float t = -m[0 * 3 + 0] - m[1 * 3 + 1] + m[2 * 3 + 2] + 1.0f; + float s = XrRcpSqrt(t) * 0.5f; + result->z = s * t; + result->w = (m[0 * 3 + 1] - m[1 * 3 + 0]) * s; + result->x = (m[2 * 3 + 0] + m[0 * 3 + 2]) * s; + result->y = (m[1 * 3 + 2] + m[2 * 3 + 1]) * s; + } +} + +// Get the scale from a combined translation(rotation(scale(object))) matrix. +inline static void XrMatrix4x4f_GetScale(XrVector3f* result, const XrMatrix4x4f* src) { + assert(XrMatrix4x4f_IsAffine(src, 1e-4f)); + assert(XrMatrix4x4f_IsOrthogonal(src, 1e-4f)); + + result->x = sqrtf(src->m[0] * src->m[0] + src->m[1] * src->m[1] + src->m[2] * src->m[2]); + result->y = sqrtf(src->m[4] * src->m[4] + src->m[5] * src->m[5] + src->m[6] * src->m[6]); + result->z = sqrtf(src->m[8] * src->m[8] + src->m[9] * src->m[9] + src->m[10] * src->m[10]); +} + +// Transforms a 3D vector. +inline static void XrMatrix4x4f_TransformVector3f(XrVector3f* result, const XrMatrix4x4f* m, const XrVector3f* v) { + const float w = m->m[3] * v->x + m->m[7] * v->y + m->m[11] * v->z + m->m[15]; + const float rcpW = 1.0f / w; + result->x = (m->m[0] * v->x + m->m[4] * v->y + m->m[8] * v->z + m->m[12]) * rcpW; + result->y = (m->m[1] * v->x + m->m[5] * v->y + m->m[9] * v->z + m->m[13]) * rcpW; + result->z = (m->m[2] * v->x + m->m[6] * v->y + m->m[10] * v->z + m->m[14]) * rcpW; +} + +// Transforms a 4D vector. +inline static void XrMatrix4x4f_TransformVector4f(XrVector4f* result, const XrMatrix4x4f* m, const XrVector4f* v) { + result->x = m->m[0] * v->x + m->m[4] * v->y + m->m[8] * v->z + m->m[12] * v->w; + result->y = m->m[1] * v->x + m->m[5] * v->y + m->m[9] * v->z + m->m[13] * v->w; + result->z = m->m[2] * v->x + m->m[6] * v->y + m->m[10] * v->z + m->m[14] * v->w; + result->w = m->m[3] * v->x + m->m[7] * v->y + m->m[11] * v->z + m->m[15] * v->w; +} + +// Transforms the 'mins' and 'maxs' bounds with the given 'matrix'. +inline static void XrMatrix4x4f_TransformBounds(XrVector3f* resultMins, XrVector3f* resultMaxs, const XrMatrix4x4f* matrix, + const XrVector3f* mins, const XrVector3f* maxs) { + assert(XrMatrix4x4f_IsAffine(matrix, 1e-4f)); + + const XrVector3f center = {(mins->x + maxs->x) * 0.5f, (mins->y + maxs->y) * 0.5f, (mins->z + maxs->z) * 0.5f}; + const XrVector3f extents = {maxs->x - center.x, maxs->y - center.y, maxs->z - center.z}; + const XrVector3f newCenter = {matrix->m[0] * center.x + matrix->m[4] * center.y + matrix->m[8] * center.z + matrix->m[12], + matrix->m[1] * center.x + matrix->m[5] * center.y + matrix->m[9] * center.z + matrix->m[13], + matrix->m[2] * center.x + matrix->m[6] * center.y + matrix->m[10] * center.z + matrix->m[14]}; + const XrVector3f newExtents = { + fabsf(extents.x * matrix->m[0]) + fabsf(extents.y * matrix->m[4]) + fabsf(extents.z * matrix->m[8]), + fabsf(extents.x * matrix->m[1]) + fabsf(extents.y * matrix->m[5]) + fabsf(extents.z * matrix->m[9]), + fabsf(extents.x * matrix->m[2]) + fabsf(extents.y * matrix->m[6]) + fabsf(extents.z * matrix->m[10])}; + XrVector3f_Sub(resultMins, &newCenter, &newExtents); + XrVector3f_Add(resultMaxs, &newCenter, &newExtents); +} + +// Returns true if the 'mins' and 'maxs' bounds is completely off to one side of the projection matrix. +inline static bool XrMatrix4x4f_CullBounds(const XrMatrix4x4f* mvp, const XrVector3f* mins, const XrVector3f* maxs) { + if (maxs->x <= mins->x && maxs->y <= mins->y && maxs->z <= mins->z) { + return false; + } + + XrVector4f c[8]; + for (int i = 0; i < 8; i++) { + const XrVector4f corner = {(i & 1) != 0 ? maxs->x : mins->x, (i & 2) != 0 ? maxs->y : mins->y, + (i & 4) != 0 ? maxs->z : mins->z, 1.0f}; + XrMatrix4x4f_TransformVector4f(&c[i], mvp, &corner); + } + + int i; + for (i = 0; i < 8; i++) { + if (c[i].x > -c[i].w) { + break; + } + } + if (i == 8) { + return true; + } + for (i = 0; i < 8; i++) { + if (c[i].x < c[i].w) { + break; + } + } + if (i == 8) { + return true; + } + + for (i = 0; i < 8; i++) { + if (c[i].y > -c[i].w) { + break; + } + } + if (i == 8) { + return true; + } + for (i = 0; i < 8; i++) { + if (c[i].y < c[i].w) { + break; + } + } + if (i == 8) { + return true; + } + for (i = 0; i < 8; i++) { + if (c[i].z > -c[i].w) { + break; + } + } + if (i == 8) { + return true; + } + for (i = 0; i < 8; i++) { + if (c[i].z < c[i].w) { + break; + } + } + return i == 8; +} + +#endif // XR_LINEAR_H_