Repository: hyperlogic/splatapult Branch: main Commit: e87960fdf4a5 Files: 96 Total size: 525.8 KB Directory structure: gitextract_t4nb5sit/ ├── .github/ │ └── workflows/ │ └── build.yaml ├── .gitignore ├── .gitmodules ├── BUILD.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── data/ │ ├── test.ply │ └── test_vr.json ├── font/ │ └── JetBrainsMono-Medium.json ├── meta-quest/ │ ├── cp_from_sdk │ └── splatapult/ │ ├── .gitignore │ ├── Projects/ │ │ └── Android/ │ │ ├── AndroidManifest.xml │ │ ├── build.bat │ │ ├── build.gradle │ │ ├── build.py │ │ ├── gradle.properties │ │ ├── jni/ │ │ │ ├── Android.mk │ │ │ └── Application.mk │ │ └── settings.gradle │ ├── assets/ │ │ ├── .gitignore │ │ └── donotedelete.txt │ └── java/ │ └── com/ │ └── oculus/ │ └── NativeActivity.java ├── shader/ │ ├── carpet_frag.glsl │ ├── carpet_vert.glsl │ ├── debugdraw_frag.glsl │ ├── debugdraw_vert.glsl │ ├── desktop_frag.glsl │ ├── desktop_vert.glsl │ ├── multi_radixsort.glsl │ ├── multi_radixsort_histograms.glsl │ ├── point_frag.glsl │ ├── point_geom.glsl │ ├── point_vert.glsl │ ├── presort_compute.glsl │ ├── single_radixsort.glsl │ ├── splat_frag.glsl │ ├── splat_geom.glsl │ ├── splat_peel_frag.glsl │ ├── splat_vert.glsl │ ├── text_frag.glsl │ └── text_vert.glsl ├── src/ │ ├── android_main.cpp │ ├── app.cpp │ ├── app.h │ ├── camerapathrenderer.cpp │ ├── camerapathrenderer.h │ ├── camerasconfig.cpp │ ├── camerasconfig.h │ ├── core/ │ │ ├── binaryattribute.cpp │ │ ├── binaryattribute.h │ │ ├── debugrenderer.cpp │ │ ├── debugrenderer.h │ │ ├── framebuffer.cpp │ │ ├── framebuffer.h │ │ ├── image.cpp │ │ ├── image.h │ │ ├── inputbuddy.cpp │ │ ├── inputbuddy.h │ │ ├── log.cpp │ │ ├── log.h │ │ ├── optionparser.h │ │ ├── program.cpp │ │ ├── program.h │ │ ├── statemachine.h │ │ ├── textrenderer.cpp │ │ ├── textrenderer.h │ │ ├── texture.cpp │ │ ├── texture.h │ │ ├── util.cpp │ │ ├── util.h │ │ ├── vertexbuffer.cpp │ │ ├── vertexbuffer.h │ │ ├── xrbuddy.cpp │ │ └── xrbuddy.h │ ├── flycam.cpp │ ├── flycam.h │ ├── gaussiancloud.cpp │ ├── gaussiancloud.h │ ├── magiccarpet.cpp │ ├── magiccarpet.h │ ├── maincontext.h │ ├── ply.cpp │ ├── ply.h │ ├── pointcloud.cpp │ ├── pointcloud.h │ ├── pointrenderer.cpp │ ├── pointrenderer.h │ ├── radix_sort.hpp │ ├── sdl_main.cpp │ ├── splatrenderer.cpp │ ├── splatrenderer.h │ ├── vrconfig.cpp │ └── vrconfig.h ├── tasks.py └── vcpkg.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yaml ================================================ name: Linux/Windows Build run-name: ${{ github.actor }} is building splatapult on: [push] jobs: build-linux: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: true - name: Install System Packages uses: ConorMacBride/install-package@v1 with: apt: libxmu-dev libxi-dev libgl-dev libxcb-glx0-dev libglu1-mesa-dev libxxf86vm-dev libxrandr-dev - name: Install CMake uses: lukka/get-cmake@v3.29.0 - name: Create Build Dir run: mkdir build - name: CMake Configure working-directory: build run: cmake -DSHIPPING=ON -DCMAKE_TOOLCHAIN_FILE="../vcpkg/scripts/buildsystems/vcpkg.cmake" .. - name: CMake Build working-directory: build run: cmake --build . --config=Release build-windows: runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: true - name: Get Microsoft Visual Studio for C++ uses: ilammy/msvc-dev-cmd@v1 - name: Install CMake uses: lukka/get-cmake@v3.29.0 - name: Create Build Dir run: mkdir build - name: CMake Configure working-directory: build run: cmake -DSHIPPING=ON -DCMAKE_TOOLCHAIN_FILE="../vcpkg/scripts/buildsystems/vcpkg.cmake" .. - name: CMake Build working-directory: build run: cmake --build . --config=Release - name: Upload Artifiacts uses: actions/upload-artifact@v4 with: name: windows-build path: build/Release ================================================ FILE: .gitignore ================================================ build/ meta-quest/ovr_openxr_mobile_sdk_59.0/ meta-quest/ovr_openxr_mobile_sdk_59.0.zip data/* !data/test.ply !data/test_vr.json vcpkg_installed/ # ignore archived release zip splatapult*.zip ================================================ FILE: .gitmodules ================================================ [submodule "vcpkg"] path = vcpkg url = https://github.com/microsoft/vcpkg ================================================ FILE: BUILD.md ================================================ Windows Build (vcpkg submodule) ----------------------- * Install Visual Studio 2022 * Install cmake 3.27.1 * Ensure splatapult has the vcpkg submodule. Either clone with the --recursive flag so that the vcpkg submodule is added or execute `git submodule init` and `git submodule update` after a regular clone. * Bootstrap vcpkg - `cd vcpkg` - `bootstrap-vcpg.bat` * Execute cmake to create a Visual Studio solution file - `mkdir build` - `cd build` - `cmake .. -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake` * Build exe by using Visual Studio by loading splatapult.sln or from the command line: - `cmake --build . --config=Release` Windows Shipping Builds ------------------------- The SHIPPING cmake option is used to create a release version of splataplut. A post build step will copy all of the the data folders (font, shader, texture) into the build directory. And the resulting exe will use that copy. You can then zip up the folder and distrubute it to users. * To create a shipping build: - `mkdir build` - `cd build` - `cmake .. -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake -DSHIPPING=ON` - `cmake --build . --config=Release` Linux Build (vcpkg submodule) -------------------- * Install dependencies - `sudo apt-get install clang` - `sudo apt-get install cmake` - `sudo apt-get install freeglut3-dev` - `sudo apt-get install libopenxr-dev` * Ensure splatapult has the vcpkg submodule. Either clone with the --recursive flag so that the vcpkg submodule is added or execute `git submodule init` and `git submodule update` after a regular clone. * Bootstrap vcpkg - `cd vcpkg` - `bootstrap-vcpg.sh` * Execute cmake to create a Makefile - `mkdir build` - `cd build` - `cmake .. -G "Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake` * build executable - `cmake --build . --config=Release` *EXPERIMENTAL* Meta Quest Build (OUT OF DATE) -------------------- NOTE: Although the quest build functions it is much to slow for most scenes. A Quest2 headset can only run a scene consisting of 25k splats. * Use vcpkg to install the following packages: - `vcpkg install glm:arm64-android` - `vcpkg install libpng:arm64-android` - `vcpkg install nlohmann-json:arm64-android` * Set the following environment var ANDROID_VCPKG_DIR to point to vcpkg/installed/arm64-android. * Download the [Meta OpenXR Mobile SDK 59.0](https://developer.oculus.com/downloads/package/oculus-openxr-mobile-sdk/) * Install [Android Studio Bumble Bee, Patch 3](https://developer.android.com/studio/archive) newer versions do not work with Meta OpenXR Mobile SDK 59.0. Follow this guide to setup [Android Studio](https://developer.oculus.com/documentation/native/android/mobile-studio-setup-android/) * Copy the ovr_openxr_mobile_sdk_59.0 dir into the meta-quest dir. * Copy the meta-quest/splatapult dir to ovr_openxr_mobile_sdk_59.0/XrSamples/splataplut * Open the ovr_openxr_mobile_sdk_59.0/XrSamples/splatapult in AndroidStudio. * Sync and Build ================================================ FILE: CMakeLists.txt ================================================ # See BUILD.md for more info # Windows cheat sheet # > mkdir build # > cd build # > cmake -DSHIPPING=ON -DCMAKE_TOOLCHAIN_FILE="C:\msys64\home\hyperlogic\code\vcpkg\scripts\buildsystems\vcpkg.cmake" .. # > cmake --build . --config Release cmake_minimum_required(VERSION 3.13 FATAL_ERROR) set(PROJECT_NAME splatapult) project(${PROJECT_NAME} LANGUAGES CXX) option(SHIPPING "Build for shipping" OFF) if(UNIX) find_package(X11 REQUIRED) endif() if(WIN32) # kind of a hack, I want to be able to include glm/glm.hpp on windows and linux include_directories(${VCPKG_INSTALLED_DIR}/x64-windows/include) endif() # opengl find_package(OpenGL REQUIRED) include_directories(${GL_INCLUDE_DIRS}) # sdl2 find_package(SDL2 CONFIG REQUIRED) # glew find_package(GLEW REQUIRED) # glm find_package(glm REQUIRED) # png find_package(PNG REQUIRED) # nlohmann-json if (WIN32) find_package(nlohmann_json CONFIG REQUIRED) endif() # eigen find_package(Eigen3 CONFIG REQUIRED) # tracy if(WIN32 AND NOT SHIPPING) find_package(Tracy CONFIG REQUIRED) add_compile_definitions(TRACY_ENABLE) set(TRACY_LIBRARIES, Tracy::TracyClient) else() set(TRACY_LIBRARIES, "") endif() # openxr-loader find_package(OpenXR CONFIG REQUIRED) # src include_directories(src) add_executable(${PROJECT_NAME} src/core/binaryattribute.cpp src/core/debugrenderer.cpp src/core/framebuffer.cpp src/core/image.cpp src/core/inputbuddy.cpp src/core/log.cpp src/core/program.cpp src/core/texture.cpp src/core/util.cpp src/core/vertexbuffer.cpp src/core/textrenderer.cpp src/core/xrbuddy.cpp src/app.cpp src/camerasconfig.cpp src/camerapathrenderer.cpp src/flycam.cpp src/gaussiancloud.cpp src/magiccarpet.cpp src/ply.cpp src/pointcloud.cpp src/pointrenderer.cpp src/sdl_main.cpp src/splatrenderer.cpp src/vrconfig.cpp ) target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) if(WIN32) # Comment this out to see SDL_Log output for debugging # set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS /SUBSYSTEM:WINDOWS) endif() if(WIN32) if(SHIPPING) target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENGL_LIBRARIES} $ $,SDL2::SDL2,SDL2::SDL2-static> GLEW::GLEW glm::glm PNG::PNG nlohmann_json::nlohmann_json Eigen3::Eigen OpenXR::headers OpenXR::openxr_loader ) else() target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENGL_LIBRARIES} $ $,SDL2::SDL2,SDL2::SDL2-static> GLEW::GLEW glm::glm PNG::PNG nlohmann_json::nlohmann_json Eigen3::Eigen Tracy::TracyClient OpenXR::headers OpenXR::openxr_loader ) endif() else() target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENGL_LIBRARIES} $ $,SDL2::SDL2,SDL2::SDL2-static> GLEW::GLEW glm::glm PNG::PNG # nlohmann_json::nlohmann_json Eigen3::Eigen OpenXR::headers OpenXR::openxr_loader ${X11_LIBRARIES} ) endif() if(SHIPPING) add_compile_definitions(SHIPPING) # Copy required data directories to the executable directory add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/font $/font COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/shader $/shader COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/texture $/texture COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/data/test.ply $/data/test.ply ) endif() ================================================ FILE: LICENSE ================================================ MIT License Copyright 2024 Anthony J. Thibault Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ Splatapult ---------------------------------------------- ![Splatapult logo](/splatapult.png) [](https://www.youtube.com/watch?v=18DuNJRZbzQ "Splatapult Demo") ![Splatapult Build](https://github.com/hyperlogic/splatapult/actions/workflows/build.yaml/badge.svg) A program to display 3d gaussian splats files splatapult [OPTIONS] ply_filename Options ------------- -v, --openxr launch app in vr mode, using openxr runtime -f, --fullscreen launch window in fullscreen -d, --debug enable debug logging --fp16 Use 16-bit half-precision floating frame buffer, to reduce color banding artifacts --fp32 Use 32-bit floating point frame buffer, to reduce color banding even more --nosh Don't load/render full sh, this will reduce memory usage and higher performance -h, --help show help Desktop Controls -------------------- * wasd - move * q, e - roll * t, g - up, down * arrow keys - look * right mouse button - hold down for mouse look. * gamepad - if present, right stick to rotate, left stick to move, bumpers to roll * c - toggle between initial SfM point cloud (if present) and gaussian splats. * n - jump to next camera * p - jump to previous camera * y - toggle rendering of camera frustums * h - toggle rendering of camera path * return - save the current position and orientation of the world into a vr.json file. VR Controls --------------- * left stick - move * right stick - snap turn * f - show hide floor carpet. * single grab - translate the world. * double grab - rotate and translate the world. * triple grab - (double grab while trigger is depressed) scale, rotate and translate the world. * c - toggle between initial SfM point cloud (if present) and gaussian splats. * y - toggle rendering of camera frustums * h - toggle rendering of camera path * return - save the current position and orientation/scale of the world into a vr.json file. Config Files ---------------------- If a "_vr.json" file is found, it will be used to determine the proper starting position, scale and orienation for vr mode. You can create your own _vr.json file by manipulating the scene via grab in vr mode, then press return to save. Splatapult supports the same dir structure that [Gaussian Splatting](https://github.com/graphdeco-inria/gaussian-splatting) code will output. Which is as follows: ``` dir/ point_cloud/ iteration_30000/ point_cloud.ply point_cloud_vr.json input.ply cameras.json ``` input.ply contains the point cloud and cameras.json will contain the camera orientations from the SfM stage. If the "cameras.json" file is found in the same dir as the ply_filename or it's parent dirs, it will be loaded. The 'n' and 'p' keys can then be used to cycle thru the camera viewpoints. It will also support files downloaded from lumalabs.ai, but in this case there will be no point clouds or cameras. ``` dir/ mycapture.ply mycapture_vr.json ``` Build Info -------------------- See [BUILD.md](BUILD.md) for information on building Splatapult from source. ================================================ FILE: data/test_vr.json ================================================ { "floorMat": [[-0.705639, -0.312374, -0.636, -0.573335], [-1.64858e-05, 0.897588, -0.440836, -1.78893], [0.708571, -0.311061, -0.633378, -0.512137], [0, 0, 0, 1]] } ================================================ FILE: font/JetBrainsMono-Medium.json ================================================ { "texture_width": 1024, "glyph_metrics": { "32": { "ascii_index": 32, "xy_lower_left": [-0.039216, -0.039216], "xy_upper_right": [0.039216, 0.039216], "uv_lower_left": [0.000000, 0.992188], "uv_upper_right": [0.007812, 1.000000], "advance": [0.451613, 0.000000] }, "33": { "ascii_index": 33, "xy_lower_left": [0.122075, -0.047280], "xy_upper_right": [0.329538, 0.595667], "uv_lower_left": [0.099609, 0.923828], "uv_upper_right": [0.123047, 1.000000], "advance": [0.451613, 0.000000] }, "34": { "ascii_index": 34, "xy_lower_left": [0.057559, 0.291429], "xy_upper_right": [0.394054, 0.595667], "uv_lower_left": [0.199219, 0.964844], "uv_upper_right": [0.238281, 1.000000], "advance": [0.451613, 0.000000] }, "35": { "ascii_index": 35, "xy_lower_left": [-0.015022, -0.039216], "xy_upper_right": [0.474700, 0.595667], "uv_lower_left": [0.298828, 0.924805], "uv_upper_right": [0.356445, 1.000000], "advance": [0.451613, 0.000000] }, "36": { "ascii_index": 36, "xy_lower_left": [0.009171, -0.144054], "xy_upper_right": [0.450506, 0.708570], "uv_lower_left": [0.398438, 0.898438], "uv_upper_right": [0.450195, 1.000000], "advance": [0.451613, 0.000000] }, "37": { "ascii_index": 37, "xy_lower_left": [-0.039216, -0.047280], "xy_upper_right": [0.490829, 0.595667], "uv_lower_left": [0.498047, 0.923828], "uv_upper_right": [0.560547, 1.000000], "advance": [0.451613, 0.000000] }, "38": { "ascii_index": 38, "xy_lower_left": [-0.023087, -0.047280], "xy_upper_right": [0.506958, 0.603732], "uv_lower_left": [0.597656, 0.922852], "uv_upper_right": [0.660156, 1.000000], "advance": [0.451613, 0.000000] }, "39": { "ascii_index": 39, "xy_lower_left": [0.146268, 0.291429], "xy_upper_right": [0.313409, 0.595667], "uv_lower_left": [0.697266, 0.964844], "uv_upper_right": [0.715820, 1.000000], "advance": [0.451613, 0.000000] }, "40": { "ascii_index": 40, "xy_lower_left": [0.097881, -0.135990], "xy_upper_right": [0.410183, 0.676312], "uv_lower_left": [0.796875, 0.903320], "uv_upper_right": [0.833008, 1.000000], "advance": [0.451613, 0.000000] }, "41": { "ascii_index": 41, "xy_lower_left": [0.041429, -0.135990], "xy_upper_right": [0.361796, 0.676312], "uv_lower_left": [0.896484, 0.903320], "uv_upper_right": [0.933594, 1.000000], "advance": [0.451613, 0.000000] }, "42": { "ascii_index": 42, "xy_lower_left": [-0.015022, 0.033365], "xy_upper_right": [0.474700, 0.515022], "uv_lower_left": [0.000000, 0.843750], "uv_upper_right": [0.057617, 0.900391], "advance": [0.451613, 0.000000] }, "43": { "ascii_index": 43, "xy_lower_left": [0.009171, 0.033365], "xy_upper_right": [0.450506, 0.474700], "uv_lower_left": [0.099609, 0.848633], "uv_upper_right": [0.151367, 0.900391], "advance": [0.451613, 0.000000] }, "44": { "ascii_index": 44, "xy_lower_left": [0.114010, -0.160183], "xy_upper_right": [0.329538, 0.152119], "uv_lower_left": [0.199219, 0.864258], "uv_upper_right": [0.223633, 0.900391], "advance": [0.451613, 0.000000] }, "45": { "ascii_index": 45, "xy_lower_left": [0.065623, 0.178526], "xy_upper_right": [0.394054, 0.329538], "uv_lower_left": [0.298828, 0.883789], "uv_upper_right": [0.336914, 0.900391], "advance": [0.451613, 0.000000] }, "46": { "ascii_index": 46, "xy_lower_left": [0.122075, -0.047280], "xy_upper_right": [0.337603, 0.160183], "uv_lower_left": [0.398438, 0.876953], "uv_upper_right": [0.422852, 0.900391], "advance": [0.451613, 0.000000] }, "47": { "ascii_index": 47, "xy_lower_left": [0.009171, -0.119861], "xy_upper_right": [0.442441, 0.676312], "uv_lower_left": [0.498047, 0.805664], "uv_upper_right": [0.548828, 0.900391], "advance": [0.451613, 0.000000] }, "48": { "ascii_index": 48, "xy_lower_left": [0.017236, -0.047280], "xy_upper_right": [0.442441, 0.603732], "uv_lower_left": [0.597656, 0.823242], "uv_upper_right": [0.647461, 0.900391], "advance": [0.451613, 0.000000] }, "49": { "ascii_index": 49, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.458571, 0.595667], "uv_lower_left": [0.697266, 0.825195], "uv_upper_right": [0.748047, 0.900391], "advance": [0.451613, 0.000000] }, "50": { "ascii_index": 50, "xy_lower_left": [0.009171, -0.039216], "xy_upper_right": [0.442441, 0.603732], "uv_lower_left": [0.796875, 0.824219], "uv_upper_right": [0.847656, 0.900391], "advance": [0.451613, 0.000000] }, "51": { "ascii_index": 51, "xy_lower_left": [0.009171, -0.047280], "xy_upper_right": [0.434377, 0.595667], "uv_lower_left": [0.896484, 0.824219], "uv_upper_right": [0.946289, 0.900391], "advance": [0.451613, 0.000000] }, "52": { "ascii_index": 52, "xy_lower_left": [0.009171, -0.039216], "xy_upper_right": [0.418248, 0.595667], "uv_lower_left": [0.000000, 0.725586], "uv_upper_right": [0.047852, 0.800781], "advance": [0.451613, 0.000000] }, "53": { "ascii_index": 53, "xy_lower_left": [0.009171, -0.047280], "xy_upper_right": [0.434377, 0.595667], "uv_lower_left": [0.099609, 0.724609], "uv_upper_right": [0.149414, 0.800781], "advance": [0.451613, 0.000000] }, "54": { "ascii_index": 54, "xy_lower_left": [0.001107, -0.047280], "xy_upper_right": [0.450506, 0.595667], "uv_lower_left": [0.199219, 0.724609], "uv_upper_right": [0.251953, 0.800781], "advance": [0.451613, 0.000000] }, "55": { "ascii_index": 55, "xy_lower_left": [0.017236, -0.039216], "xy_upper_right": [0.458571, 0.595667], "uv_lower_left": [0.298828, 0.725586], "uv_upper_right": [0.350586, 0.800781], "advance": [0.451613, 0.000000] }, "56": { "ascii_index": 56, "xy_lower_left": [0.001107, -0.047280], "xy_upper_right": [0.450506, 0.603732], "uv_lower_left": [0.398438, 0.723633], "uv_upper_right": [0.451172, 0.800781], "advance": [0.451613, 0.000000] }, "57": { "ascii_index": 57, "xy_lower_left": [0.001107, -0.039216], "xy_upper_right": [0.450506, 0.603732], "uv_lower_left": [0.498047, 0.724609], "uv_upper_right": [0.550781, 0.800781], "advance": [0.451613, 0.000000] }, "58": { "ascii_index": 58, "xy_lower_left": [0.122075, -0.047280], "xy_upper_right": [0.337603, 0.466635], "uv_lower_left": [0.597656, 0.740234], "uv_upper_right": [0.622070, 0.800781], "advance": [0.451613, 0.000000] }, "59": { "ascii_index": 59, "xy_lower_left": [0.105946, -0.168248], "xy_upper_right": [0.337603, 0.466635], "uv_lower_left": [0.697266, 0.725586], "uv_upper_right": [0.723633, 0.800781], "advance": [0.451613, 0.000000] }, "60": { "ascii_index": 60, "xy_lower_left": [0.017236, 0.001107], "xy_upper_right": [0.434377, 0.498893], "uv_lower_left": [0.796875, 0.742188], "uv_upper_right": [0.845703, 0.800781], "advance": [0.451613, 0.000000] }, "61": { "ascii_index": 61, "xy_lower_left": [0.017236, 0.089817], "xy_upper_right": [0.434377, 0.426312], "uv_lower_left": [0.896484, 0.761719], "uv_upper_right": [0.945312, 0.800781], "advance": [0.451613, 0.000000] }, "62": { "ascii_index": 62, "xy_lower_left": [0.017236, 0.001107], "xy_upper_right": [0.434377, 0.498893], "uv_lower_left": [0.000000, 0.642578], "uv_upper_right": [0.048828, 0.701172], "advance": [0.451613, 0.000000] }, "63": { "ascii_index": 63, "xy_lower_left": [0.049494, -0.047280], "xy_upper_right": [0.410183, 0.595667], "uv_lower_left": [0.099609, 0.625000], "uv_upper_right": [0.141602, 0.701172], "advance": [0.451613, 0.000000] }, "64": { "ascii_index": 64, "xy_lower_left": [-0.015022, -0.176312], "xy_upper_right": [0.466635, 0.603732], "uv_lower_left": [0.199219, 0.608398], "uv_upper_right": [0.255859, 0.701172], "advance": [0.451613, 0.000000] }, "65": { "ascii_index": 65, "xy_lower_left": [-0.006958, -0.039216], "xy_upper_right": [0.466635, 0.595667], "uv_lower_left": [0.298828, 0.625977], "uv_upper_right": [0.354492, 0.701172], "advance": [0.451613, 0.000000] }, "66": { "ascii_index": 66, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.450506, 0.595667], "uv_lower_left": [0.398438, 0.625977], "uv_upper_right": [0.448242, 0.701172], "advance": [0.451613, 0.000000] }, "67": { "ascii_index": 67, "xy_lower_left": [0.025300, -0.047280], "xy_upper_right": [0.442441, 0.603732], "uv_lower_left": [0.498047, 0.624023], "uv_upper_right": [0.546875, 0.701172], "advance": [0.451613, 0.000000] }, "68": { "ascii_index": 68, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.434377, 0.595667], "uv_lower_left": [0.597656, 0.625977], "uv_upper_right": [0.645508, 0.701172], "advance": [0.451613, 0.000000] }, "69": { "ascii_index": 69, "xy_lower_left": [0.033365, -0.039216], "xy_upper_right": [0.442441, 0.595667], "uv_lower_left": [0.697266, 0.625977], "uv_upper_right": [0.745117, 0.701172], "advance": [0.451613, 0.000000] }, "70": { "ascii_index": 70, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.442441, 0.595667], "uv_lower_left": [0.796875, 0.625977], "uv_upper_right": [0.845703, 0.701172], "advance": [0.451613, 0.000000] }, "71": { "ascii_index": 71, "xy_lower_left": [0.017236, -0.047280], "xy_upper_right": [0.442441, 0.603732], "uv_lower_left": [0.896484, 0.624023], "uv_upper_right": [0.946289, 0.701172], "advance": [0.451613, 0.000000] }, "72": { "ascii_index": 72, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.434377, 0.595667], "uv_lower_left": [0.000000, 0.526367], "uv_upper_right": [0.047852, 0.601562], "advance": [0.451613, 0.000000] }, "73": { "ascii_index": 73, "xy_lower_left": [0.033365, -0.039216], "xy_upper_right": [0.418248, 0.595667], "uv_lower_left": [0.099609, 0.526367], "uv_upper_right": [0.144531, 0.601562], "advance": [0.451613, 0.000000] }, "74": { "ascii_index": 74, "xy_lower_left": [-0.015022, -0.047280], "xy_upper_right": [0.418248, 0.595667], "uv_lower_left": [0.199219, 0.525391], "uv_upper_right": [0.250000, 0.601562], "advance": [0.451613, 0.000000] }, "75": { "ascii_index": 75, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.466635, 0.595667], "uv_lower_left": [0.298828, 0.526367], "uv_upper_right": [0.350586, 0.601562], "advance": [0.451613, 0.000000] }, "76": { "ascii_index": 76, "xy_lower_left": [0.057559, -0.039216], "xy_upper_right": [0.458571, 0.595667], "uv_lower_left": [0.398438, 0.526367], "uv_upper_right": [0.445312, 0.601562], "advance": [0.451613, 0.000000] }, "77": { "ascii_index": 77, "xy_lower_left": [0.009171, -0.039216], "xy_upper_right": [0.450506, 0.595667], "uv_lower_left": [0.498047, 0.526367], "uv_upper_right": [0.549805, 0.601562], "advance": [0.451613, 0.000000] }, "78": { "ascii_index": 78, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.434377, 0.595667], "uv_lower_left": [0.597656, 0.526367], "uv_upper_right": [0.645508, 0.601562], "advance": [0.451613, 0.000000] }, "79": { "ascii_index": 79, "xy_lower_left": [0.017236, -0.047280], "xy_upper_right": [0.434377, 0.603732], "uv_lower_left": [0.697266, 0.524414], "uv_upper_right": [0.746094, 0.601562], "advance": [0.451613, 0.000000] }, "80": { "ascii_index": 80, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.458571, 0.595667], "uv_lower_left": [0.796875, 0.526367], "uv_upper_right": [0.847656, 0.601562], "advance": [0.451613, 0.000000] }, "81": { "ascii_index": 81, "xy_lower_left": [0.017236, -0.176312], "xy_upper_right": [0.442441, 0.603732], "uv_lower_left": [0.896484, 0.508789], "uv_upper_right": [0.946289, 0.601562], "advance": [0.451613, 0.000000] }, "82": { "ascii_index": 82, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.458571, 0.595667], "uv_lower_left": [0.000000, 0.426758], "uv_upper_right": [0.050781, 0.501953], "advance": [0.451613, 0.000000] }, "83": { "ascii_index": 83, "xy_lower_left": [0.009171, -0.047280], "xy_upper_right": [0.450506, 0.603732], "uv_lower_left": [0.099609, 0.424805], "uv_upper_right": [0.151367, 0.501953], "advance": [0.451613, 0.000000] }, "84": { "ascii_index": 84, "xy_lower_left": [-0.006958, -0.039216], "xy_upper_right": [0.458571, 0.595667], "uv_lower_left": [0.199219, 0.426758], "uv_upper_right": [0.253906, 0.501953], "advance": [0.451613, 0.000000] }, "85": { "ascii_index": 85, "xy_lower_left": [0.025300, -0.047280], "xy_upper_right": [0.434377, 0.595667], "uv_lower_left": [0.298828, 0.425781], "uv_upper_right": [0.346680, 0.501953], "advance": [0.451613, 0.000000] }, "86": { "ascii_index": 86, "xy_lower_left": [-0.006958, -0.039216], "xy_upper_right": [0.466635, 0.595667], "uv_lower_left": [0.398438, 0.426758], "uv_upper_right": [0.454102, 0.501953], "advance": [0.451613, 0.000000] }, "87": { "ascii_index": 87, "xy_lower_left": [-0.031151, -0.039216], "xy_upper_right": [0.482764, 0.595667], "uv_lower_left": [0.498047, 0.426758], "uv_upper_right": [0.558594, 0.501953], "advance": [0.451613, 0.000000] }, "88": { "ascii_index": 88, "xy_lower_left": [-0.015022, -0.039216], "xy_upper_right": [0.466635, 0.595667], "uv_lower_left": [0.597656, 0.426758], "uv_upper_right": [0.654297, 0.501953], "advance": [0.451613, 0.000000] }, "89": { "ascii_index": 89, "xy_lower_left": [-0.015022, -0.039216], "xy_upper_right": [0.474700, 0.595667], "uv_lower_left": [0.697266, 0.426758], "uv_upper_right": [0.754883, 0.501953], "advance": [0.451613, 0.000000] }, "90": { "ascii_index": 90, "xy_lower_left": [0.017236, -0.039216], "xy_upper_right": [0.434377, 0.595667], "uv_lower_left": [0.796875, 0.426758], "uv_upper_right": [0.845703, 0.501953], "advance": [0.451613, 0.000000] }, "91": { "ascii_index": 91, "xy_lower_left": [0.105946, -0.119861], "xy_upper_right": [0.385990, 0.676312], "uv_lower_left": [0.896484, 0.407227], "uv_upper_right": [0.928711, 0.501953], "advance": [0.451613, 0.000000] }, "92": { "ascii_index": 92, "xy_lower_left": [0.009171, -0.119861], "xy_upper_right": [0.442441, 0.676312], "uv_lower_left": [0.000000, 0.307617], "uv_upper_right": [0.050781, 0.402344], "advance": [0.451613, 0.000000] }, "93": { "ascii_index": 93, "xy_lower_left": [0.065623, -0.119861], "xy_upper_right": [0.345667, 0.676312], "uv_lower_left": [0.099609, 0.307617], "uv_upper_right": [0.131836, 0.402344], "advance": [0.451613, 0.000000] }, "94": { "ascii_index": 94, "xy_lower_left": [0.017236, 0.218849], "xy_upper_right": [0.442441, 0.595667], "uv_lower_left": [0.199219, 0.358398], "uv_upper_right": [0.249023, 0.402344], "advance": [0.451613, 0.000000] }, "95": { "ascii_index": 95, "xy_lower_left": [0.001107, -0.119861], "xy_upper_right": [0.450506, 0.031151], "uv_lower_left": [0.298828, 0.385742], "uv_upper_right": [0.351562, 0.402344], "advance": [0.451613, 0.000000] }, "96": { "ascii_index": 96, "xy_lower_left": [0.073688, 0.452720], "xy_upper_right": [0.329538, 0.644054], "uv_lower_left": [0.398438, 0.380859], "uv_upper_right": [0.427734, 0.402344], "advance": [0.451613, 0.000000] }, "97": { "ascii_index": 97, "xy_lower_left": [0.009171, -0.047280], "xy_upper_right": [0.434377, 0.466635], "uv_lower_left": [0.498047, 0.341797], "uv_upper_right": [0.547852, 0.402344], "advance": [0.451613, 0.000000] }, "98": { "ascii_index": 98, "xy_lower_left": [0.025300, -0.047280], "xy_upper_right": [0.434377, 0.595667], "uv_lower_left": [0.597656, 0.326172], "uv_upper_right": [0.645508, 0.402344], "advance": [0.451613, 0.000000] }, "99": { "ascii_index": 99, "xy_lower_left": [0.017236, -0.047280], "xy_upper_right": [0.442441, 0.466635], "uv_lower_left": [0.697266, 0.341797], "uv_upper_right": [0.747070, 0.402344], "advance": [0.451613, 0.000000] }, "100": { "ascii_index": 100, "xy_lower_left": [0.017236, -0.047280], "xy_upper_right": [0.434377, 0.595667], "uv_lower_left": [0.796875, 0.326172], "uv_upper_right": [0.845703, 0.402344], "advance": [0.451613, 0.000000] }, "101": { "ascii_index": 101, "xy_lower_left": [0.017236, -0.047280], "xy_upper_right": [0.434377, 0.466635], "uv_lower_left": [0.896484, 0.341797], "uv_upper_right": [0.945312, 0.402344], "advance": [0.451613, 0.000000] }, "102": { "ascii_index": 102, "xy_lower_left": [-0.006958, -0.039216], "xy_upper_right": [0.450506, 0.595667], "uv_lower_left": [0.000000, 0.227539], "uv_upper_right": [0.053711, 0.302734], "advance": [0.451613, 0.000000] }, "103": { "ascii_index": 103, "xy_lower_left": [0.017236, -0.176312], "xy_upper_right": [0.434377, 0.466635], "uv_lower_left": [0.099609, 0.226562], "uv_upper_right": [0.148438, 0.302734], "advance": [0.451613, 0.000000] }, "104": { "ascii_index": 104, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.434377, 0.595667], "uv_lower_left": [0.199219, 0.227539], "uv_upper_right": [0.247070, 0.302734], "advance": [0.451613, 0.000000] }, "105": { "ascii_index": 105, "xy_lower_left": [0.017236, -0.039216], "xy_upper_right": [0.466635, 0.627925], "uv_lower_left": [0.298828, 0.223633], "uv_upper_right": [0.351562, 0.302734], "advance": [0.451613, 0.000000] }, "106": { "ascii_index": 106, "xy_lower_left": [0.017236, -0.176312], "xy_upper_right": [0.385990, 0.627925], "uv_lower_left": [0.398438, 0.207031], "uv_upper_right": [0.441406, 0.302734], "advance": [0.451613, 0.000000] }, "107": { "ascii_index": 107, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.466635, 0.595667], "uv_lower_left": [0.498047, 0.227539], "uv_upper_right": [0.549805, 0.302734], "advance": [0.451613, 0.000000] }, "108": { "ascii_index": 108, "xy_lower_left": [-0.023087, -0.039216], "xy_upper_right": [0.458571, 0.595667], "uv_lower_left": [0.597656, 0.227539], "uv_upper_right": [0.654297, 0.302734], "advance": [0.451613, 0.000000] }, "109": { "ascii_index": 109, "xy_lower_left": [0.001107, -0.039216], "xy_upper_right": [0.458571, 0.466635], "uv_lower_left": [0.697266, 0.243164], "uv_upper_right": [0.750977, 0.302734], "advance": [0.451613, 0.000000] }, "110": { "ascii_index": 110, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.434377, 0.466635], "uv_lower_left": [0.796875, 0.243164], "uv_upper_right": [0.844727, 0.302734], "advance": [0.451613, 0.000000] }, "111": { "ascii_index": 111, "xy_lower_left": [0.017236, -0.047280], "xy_upper_right": [0.434377, 0.466635], "uv_lower_left": [0.896484, 0.242188], "uv_upper_right": [0.945312, 0.302734], "advance": [0.451613, 0.000000] }, "112": { "ascii_index": 112, "xy_lower_left": [0.025300, -0.176312], "xy_upper_right": [0.434377, 0.466635], "uv_lower_left": [0.000000, 0.126953], "uv_upper_right": [0.047852, 0.203125], "advance": [0.451613, 0.000000] }, "113": { "ascii_index": 113, "xy_lower_left": [0.017236, -0.176312], "xy_upper_right": [0.434377, 0.466635], "uv_lower_left": [0.099609, 0.126953], "uv_upper_right": [0.148438, 0.203125], "advance": [0.451613, 0.000000] }, "114": { "ascii_index": 114, "xy_lower_left": [0.041429, -0.039216], "xy_upper_right": [0.450506, 0.466635], "uv_lower_left": [0.199219, 0.143555], "uv_upper_right": [0.247070, 0.203125], "advance": [0.451613, 0.000000] }, "115": { "ascii_index": 115, "xy_lower_left": [0.017236, -0.047280], "xy_upper_right": [0.434377, 0.466635], "uv_lower_left": [0.298828, 0.142578], "uv_upper_right": [0.347656, 0.203125], "advance": [0.451613, 0.000000] }, "116": { "ascii_index": 116, "xy_lower_left": [-0.006958, -0.039216], "xy_upper_right": [0.442441, 0.579538], "uv_lower_left": [0.398438, 0.129883], "uv_upper_right": [0.451172, 0.203125], "advance": [0.451613, 0.000000] }, "117": { "ascii_index": 117, "xy_lower_left": [0.025300, -0.047280], "xy_upper_right": [0.434377, 0.458571], "uv_lower_left": [0.498047, 0.143555], "uv_upper_right": [0.545898, 0.203125], "advance": [0.451613, 0.000000] }, "118": { "ascii_index": 118, "xy_lower_left": [-0.006958, -0.039216], "xy_upper_right": [0.458571, 0.458571], "uv_lower_left": [0.597656, 0.144531], "uv_upper_right": [0.652344, 0.203125], "advance": [0.451613, 0.000000] }, "119": { "ascii_index": 119, "xy_lower_left": [-0.023087, -0.039216], "xy_upper_right": [0.474700, 0.458571], "uv_lower_left": [0.697266, 0.144531], "uv_upper_right": [0.755859, 0.203125], "advance": [0.451613, 0.000000] }, "120": { "ascii_index": 120, "xy_lower_left": [-0.006958, -0.039216], "xy_upper_right": [0.466635, 0.458571], "uv_lower_left": [0.796875, 0.144531], "uv_upper_right": [0.852539, 0.203125], "advance": [0.451613, 0.000000] }, "121": { "ascii_index": 121, "xy_lower_left": [-0.006958, -0.176312], "xy_upper_right": [0.458571, 0.458571], "uv_lower_left": [0.896484, 0.127930], "uv_upper_right": [0.951172, 0.203125], "advance": [0.451613, 0.000000] }, "122": { "ascii_index": 122, "xy_lower_left": [0.025300, -0.039216], "xy_upper_right": [0.434377, 0.458571], "uv_lower_left": [0.000000, 0.044922], "uv_upper_right": [0.047852, 0.103516], "advance": [0.451613, 0.000000] }, "123": { "ascii_index": 123, "xy_lower_left": [0.009171, -0.119861], "xy_upper_right": [0.426312, 0.676312], "uv_lower_left": [0.099609, 0.008789], "uv_upper_right": [0.148438, 0.103516], "advance": [0.451613, 0.000000] }, "124": { "ascii_index": 124, "xy_lower_left": [0.146268, -0.119861], "xy_upper_right": [0.305345, 0.676312], "uv_lower_left": [0.199219, 0.008789], "uv_upper_right": [0.216797, 0.103516], "advance": [0.451613, 0.000000] }, "125": { "ascii_index": 125, "xy_lower_left": [0.025300, -0.119861], "xy_upper_right": [0.442441, 0.676312], "uv_lower_left": [0.298828, 0.008789], "uv_upper_right": [0.347656, 0.103516], "advance": [0.451613, 0.000000] }, "126": { "ascii_index": 126, "xy_lower_left": [0.009171, 0.146268], "xy_upper_right": [0.450506, 0.394054], "uv_lower_left": [0.398438, 0.075195], "uv_upper_right": [0.450195, 0.103516], "advance": [0.451613, 0.000000] } }, "kerning": { } } ================================================ FILE: meta-quest/cp_from_sdk ================================================ rm -rf splatapult cp -r ovr_openxr_mobile_sdk_59.0/XrSamples/splatapult splatapult ================================================ FILE: meta-quest/splatapult/.gitignore ================================================ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Log/OS Files *.log # Android Studio generated files and folders captures/ .externalNativeBuild/ .cxx/ *.apk output.json # IntelliJ *.iml .idea/ misc.xml deploymentTargetDropDown.xml render.experimental.xml # Keystore files *.jks *.keystore # Google Services (e.g. APIs or Firebase) google-services.json # Android Profiling *.hprof ================================================ FILE: meta-quest/splatapult/Projects/Android/AndroidManifest.xml ================================================ ================================================ FILE: meta-quest/splatapult/Projects/Android/build.bat ================================================ @rem Only edit the master copy of this file in SDK_ROOT/bin/scripts/build/perproject @setlocal enableextensions enabledelayedexpansion @if not exist "build.gradle" ( @echo Build script must be executed from project directory. & goto :Abort ) @set P=.. :TryAgain @rem @echo P = %P% @if exist "%P%\bin\scripts\build\build.py.bat" goto :Found @if exist "%P%\bin\scripts\build" @echo "Could not find build.py.bat" & goto :Abort @set P=%P%\.. @goto :TryAgain :Found @set P=%P%\bin\scripts\build @call %P%\build.py.bat %1 %2 %3 %4 %5 @goto :End :Abort :End ================================================ FILE: meta-quest/splatapult/Projects/Android/build.gradle ================================================ apply plugin: 'com.android.application' apply from: "${rootProject.projectDir}/VrApp.gradle" task copyFiles(type: Copy) { copy { from '../../../../../../shader' into '../../assets/shader' } copy { from '../../../../../../font' into '../../assets/font' } copy { from '../../../../../../texture' into '../../assets/texture' } copy { from '../../../../../../data/sh_test' into '../../assets/data/sh_test' } copy { from '../../../../../../data/livingroom' into '../../assets/data/livingroom' } } gradle.projectsEvaluated { preBuild.dependsOn(copyFiles) } android { // This is the name of the generated apk file, which will have // -debug.apk or -release.apk appended to it. // The filename doesn't effect the Android installation process. // Use only letters to remain compatible with the package name. project.archivesBaseName = "splatapult" defaultConfig { // Gradle replaces the manifest package with this value, which must // be unique on a system. If you don't change it, a new app // will replace an older one. applicationId "com.oculus.sdk." + project.archivesBaseName minSdkVersion 24 targetSdkVersion 25 compileSdkVersion 26 // override app plugin abiFilters for 64-bit support externalNativeBuild { ndk { abiFilters 'arm64-v8a' } ndkBuild { abiFilters 'arm64-v8a' } } } sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['../../java'] jniLibs.srcDir 'libs' assets.srcDirs = ['../../assets'] } } lintOptions { disable 'ExpiredTargetSdkVersion' } } ================================================ FILE: meta-quest/splatapult/Projects/Android/build.py ================================================ #!/usr/bin/python # (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. # This first bit of code is common bootstrapping code # to determine the SDK root, and to set up the import # path for additional python code. # begin bootstrap import os import sys def init(): root = os.path.realpath(os.path.dirname(os.path.realpath(__file__))) os.chdir(root) # make sure we are always executing from the project directory while os.path.isdir(os.path.join(root, "bin/scripts/build")) == False: root = os.path.realpath(os.path.join(root, "..")) if ( len(root) <= 5 ): # Should catch both Posix and Windows root directories (e.g. '/' and 'C:\') print("Unable to find SDK root. Exiting.") sys.exit(1) root = os.path.abspath(root) os.environ["OCULUS_SDK_PATH"] = root sys.path.append(root + "/bin/scripts/build") init() import ovrbuild ovrbuild.init() # end bootstrap ovrbuild.build() ================================================ FILE: meta-quest/splatapult/Projects/Android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4g ================================================ FILE: meta-quest/splatapult/Projects/Android/jni/Android.mk ================================================ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := splatapult LOCAL_CFLAGS += -Werror # need execptions for json and radix sort. LOCAL_CFLAGS += -fexceptions # This should be set via an environment var # ANDROID_VCPKG_DIR := C:/msys64/home/hyperlogic/code/vcpkg/installed/arm64-android LOCAL_C_INCLUDES := \ $(LOCAL_PATH)/../../../../../../../src/ \ $(LOCAL_PATH)/../../../../../1stParty/OVR/Include \ $(LOCAL_PATH)/../../../../../OpenXr/Include \ $(LOCAL_PATH)/../../../../../3rdParty/khronos/openxr/OpenXR-SDK/include/ \ $(LOCAL_PATH)/../../../../../3rdParty/khronos/openxr/OpenXR-SDK/src/common/ \ $(ANDROID_VCPKG_DIR)/include \ LOCAL_SRC_PATH := ../../../../../../../src LOCAL_SRC_FILES := $(LOCAL_SRC_PATH)/core/debugrenderer.cpp \ $(LOCAL_SRC_PATH)/core/image.cpp \ $(LOCAL_SRC_PATH)/core/log.cpp \ $(LOCAL_SRC_PATH)/core/program.cpp \ $(LOCAL_SRC_PATH)/core/texture.cpp \ $(LOCAL_SRC_PATH)/core/util.cpp \ $(LOCAL_SRC_PATH)/core/vertexbuffer.cpp \ $(LOCAL_SRC_PATH)/core/textrenderer.cpp \ $(LOCAL_SRC_PATH)/core/xrbuddy.cpp \ $(LOCAL_SRC_PATH)/app.cpp \ $(LOCAL_SRC_PATH)/android_main.cpp \ $(LOCAL_SRC_PATH)/camerasconfig.cpp \ $(LOCAL_SRC_PATH)/flycam.cpp \ $(LOCAL_SRC_PATH)/gaussiancloud.cpp \ $(LOCAL_SRC_PATH)/magiccarpet.cpp \ $(LOCAL_SRC_PATH)/ply.cpp \ $(LOCAL_SRC_PATH)/pointcloud.cpp \ $(LOCAL_SRC_PATH)/pointrenderer.cpp \ $(LOCAL_SRC_PATH)/splatrenderer.cpp \ $(LOCAL_SRC_PATH)/vrconfig.cpp \ LOCAL_LDLIBS := -lEGL -lGLESv3 -landroid -llog -lpng -lpng16 -lz LOCAL_LDFLAGS := -u ANativeActivity_onCreate LOCAL_LDLIBS += -L$(ANDROID_VCPKG_DIR)/lib LOCAL_STATIC_LIBRARIES := android_native_app_glue LOCAL_SHARED_LIBRARIES := openxr_loader include $(BUILD_SHARED_LIBRARY) $(call import-module,OpenXR/Projects/AndroidPrebuilt/jni) $(call import-module,android/native_app_glue) ================================================ FILE: meta-quest/splatapult/Projects/Android/jni/Application.mk ================================================ # MAKEFILE_LIST specifies the current used Makefiles, of which this is the last # one. I use that to obtain the Application.mk dir then import the root # Application.mk. ROOT_DIR := $(dir $(lastword $(MAKEFILE_LIST)))../../../../../ NDK_MODULE_PATH := $(ROOT_DIR) # ndk-r14 introduced failure for missing dependencies. If 'false', the clean # step will error as we currently remove prebuilt artifacts on clean. APP_ALLOW_MISSING_DEPS=true ================================================ FILE: meta-quest/splatapult/Projects/Android/settings.gradle ================================================ rootProject.projectDir = new File(settingsDir, '../../../..') rootProject.name = "splatapult" include ':', \ ':XrSamples:splatapult:Projects:Android' ================================================ FILE: meta-quest/splatapult/assets/.gitignore ================================================ font/ shader/ texture/ data/ ================================================ FILE: meta-quest/splatapult/assets/donotedelete.txt ================================================ ================================================ FILE: meta-quest/splatapult/java/com/oculus/NativeActivity.java ================================================ // Copyright (c) Facebook Technologies, LLC and its affiliates. All Rights reserved. package com.oculus; import android.content.pm.PackageManager; import android.os.Bundle; import android.util.Log; /** * When using NativeActivity, we currently need to handle loading of dependent shared libraries * manually before a shared library that depends on them is loaded, since there is not currently a * way to specify a shared library dependency for NativeActivity via the manifest meta-data. * *

The simplest method for doing so is to subclass NativeActivity with an empty activity that * calls System.loadLibrary on the dependent libraries, which is unfortunate when the goal is to * write a pure native C/C++ only Android activity. * *

A native-code only solution is to load the dependent libraries dynamically using dlopen(). * However, there are a few considerations, see: * https://groups.google.com/forum/#!msg/android-ndk/l2E2qh17Q6I/wj6s_6HSjaYJ * *

1. Only call dlopen() if you're sure it will succeed as the bionic dynamic linker will * remember if dlopen failed and will not re-try a dlopen on the same lib a second time. * *

2. Must remember what libraries have already been loaded to avoid infinitely looping when * libraries have circular dependencies. */ public class NativeActivity extends android.app.NativeActivity { private static final String PERMISSION_USE_SCENE = "com.oculus.permission.USE_SCENE"; private static final int REQUEST_CODE_PERMISSION_USE_SCENE = 1; private static final String TAG = "splatapult"; static { System.loadLibrary("openxr_loader"); System.loadLibrary("splatapult"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestScenePermissionIfNeeded(); } private void requestScenePermissionIfNeeded() { Log.d(TAG, "requestScenePermissionIfNeeded"); /* if (checkSelfPermission(PERMISSION_USE_SCENE) != PackageManager.PERMISSION_GRANTED) { Log.d(TAG, "Permission has not been granted, request " + PERMISSION_USE_SCENE); requestPermissions( new String[] {PERMISSION_USE_SCENE}, REQUEST_CODE_PERMISSION_USE_SCENE); } */ } } ================================================ FILE: shader/carpet_frag.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // // fullbright textured mesh // /*%%HEADER%%*/ uniform sampler2D colorTex; in vec2 frag_uv; out vec4 out_color; void main() { vec4 texColor = texture(colorTex, frag_uv); // premultiplied alpha blending out_color.rgb = texColor.a * texColor.rgb; out_color.a = texColor.a; } ================================================ FILE: shader/carpet_vert.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // // fullbright textured mesh // /*%%HEADER%%*/ uniform mat4 modelViewProjMat; in vec3 position; in vec2 uv; out vec2 frag_uv; void main(void) { gl_Position = modelViewProjMat * vec4(position, 1); frag_uv = uv; } ================================================ FILE: shader/debugdraw_frag.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // // No lighting at all, solid color // /*%%HEADER%%*/ in vec4 frag_color; out vec4 out_color; void main() { // pre-multiplied alpha blending out_color.rgb = frag_color.a * frag_color.rgb; out_color.a = frag_color.a; } ================================================ FILE: shader/debugdraw_vert.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // // No lighting at all, solid color // /*%%HEADER%%*/ uniform mat4 modelViewProjMat; in vec3 position; in vec4 color; out vec4 frag_color; void main(void) { gl_Position = modelViewProjMat * vec4(position, 1); frag_color = color; } ================================================ FILE: shader/desktop_frag.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ /*%%HEADER%%*/ /*%%DEFINES%%*/ uniform vec4 color; uniform sampler2D colorTexture; in vec2 frag_uv; out vec4 out_color; void main(void) { #ifdef USE_SUPERSAMPLING // per pixel screen space partial derivatives vec2 dx = dFdx(frag_uv) * 0.25; // horizontal offset vec2 dy = dFdy(frag_uv) * 0.25; // vertical offset // supersampled 2x2 ordered grid vec4 texColor = vec4(0); texColor += texture(colorTexture, vec2(frag_uv + dx + dy)); texColor += texture(colorTexture, vec2(frag_uv - dx + dy)); texColor += texture(colorTexture, vec2(frag_uv + dx - dy)); texColor += texture(colorTexture, vec2(frag_uv - dx - dy)); texColor *= 0.25; #else vec4 texColor = texture(colorTexture, frag_uv); #endif // premultiplied alpha blending out_color.rgb = color.a * color.rgb * texColor.rgb; out_color.a = color.a * texColor.a; } ================================================ FILE: shader/desktop_vert.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // // fullbright textured mesh // /*%%HEADER%%*/ uniform mat4 modelViewProjMat; in vec3 position; in vec2 uv; out vec2 frag_uv; void main(void) { gl_Position = modelViewProjMat * vec4(position, 1); frag_uv = uv; } ================================================ FILE: shader/multi_radixsort.glsl ================================================ /** * VkRadixSort written by Mirco Werner: https://github.com/MircoWerner/VkRadixSort * Based on implementation of Intel's Embree: https://github.com/embree/embree/blob/v4.0.0-ploc/kernels/rthwif/builder/gpu/sort.h */ #version 460 //#extension GL_GOOGLE_include_directive: enable #extension GL_KHR_shader_subgroup_basic: enable #extension GL_KHR_shader_subgroup_arithmetic: enable #extension GL_KHR_shader_subgroup_ballot: enable #define WORKGROUP_SIZE 256// assert WORKGROUP_SIZE >= RADIX_SORT_BINS #define RADIX_SORT_BINS 256 #define SUBGROUP_SIZE 32// 32 NVIDIA; 64 AMD #define BITS 32// sorting uint32_t layout (local_size_x = WORKGROUP_SIZE) in; uniform uint g_num_elements; uniform uint g_shift; uniform uint g_num_workgroups; uniform uint g_num_blocks_per_workgroup; layout (std430, binding = 0) buffer elements_in { uint g_elements_in[]; }; layout (std430, binding = 1) buffer elements_out { uint g_elements_out[]; }; layout (std430, binding = 2) buffer indices_in { uint g_indices_in[]; }; layout (std430, binding = 3) buffer indices_out { uint g_indices_out[]; }; layout (std430, binding = 4) buffer histograms { // [histogram_of_workgroup_0 | histogram_of_workgroup_1 | ... ] uint g_histograms[];// |g_histograms| = RADIX_SORT_BINS * #WORKGROUPS = RADIX_SORT_BINS * g_num_workgroups }; shared uint[RADIX_SORT_BINS / SUBGROUP_SIZE] sums;// subgroup reductions shared uint[RADIX_SORT_BINS] global_offsets;// global exclusive scan (prefix sum) struct BinFlags { uint flags[WORKGROUP_SIZE / BITS]; }; shared BinFlags[RADIX_SORT_BINS] bin_flags; void main() { uint gID = gl_GlobalInvocationID.x; uint lID = gl_LocalInvocationID.x; uint wID = gl_WorkGroupID.x; uint sID = gl_SubgroupID; uint lsID = gl_SubgroupInvocationID; uint local_histogram = 0; uint prefix_sum = 0; uint histogram_count = 0; if (lID < RADIX_SORT_BINS) { uint count = 0; for (uint j = 0; j < g_num_workgroups; j++) { const uint t = g_histograms[RADIX_SORT_BINS * j + lID]; local_histogram = (j == wID) ? count : local_histogram; count += t; } histogram_count = count; const uint sum = subgroupAdd(histogram_count); prefix_sum = subgroupExclusiveAdd(histogram_count); if (subgroupElect()) { // one thread inside the warp/subgroup enters this section sums[sID] = sum; } } barrier(); if (lID < RADIX_SORT_BINS) { const uint sums_prefix_sum = subgroupBroadcast(subgroupExclusiveAdd(sums[lsID]), sID); const uint global_histogram = sums_prefix_sum + prefix_sum; global_offsets[lID] = global_histogram + local_histogram; } // ==== scatter keys according to global offsets ===== const uint flags_bin = lID / BITS; const uint flags_bit = 1 << (lID % BITS); for (uint index = 0; index < g_num_blocks_per_workgroup; index++) { uint elementId = wID * g_num_blocks_per_workgroup * WORKGROUP_SIZE + index * WORKGROUP_SIZE + lID; // initialize bin flags if (lID < RADIX_SORT_BINS) { for (int i = 0; i < WORKGROUP_SIZE / BITS; i++) { bin_flags[lID].flags[i] = 0U;// init all bin flags to 0 } } barrier(); uint element_in = 0; uint index_in = 0; uint binID = 0; uint binOffset = 0; if (elementId < g_num_elements) { element_in = g_elements_in[elementId]; index_in = g_indices_in[elementId]; binID = (element_in >> g_shift) & uint(RADIX_SORT_BINS - 1); // offset for group binOffset = global_offsets[binID]; // add bit to flag atomicAdd(bin_flags[binID].flags[flags_bin], flags_bit); } barrier(); if (elementId < g_num_elements) { // calculate output index of element uint prefix = 0; uint count = 0; for (uint i = 0; i < WORKGROUP_SIZE / BITS; i++) { const uint bits = bin_flags[binID].flags[i]; const uint full_count = bitCount(bits); const uint partial_count = bitCount(bits & (flags_bit - 1)); prefix += (i < flags_bin) ? full_count : 0U; prefix += (i == flags_bin) ? partial_count : 0U; count += full_count; } g_elements_out[binOffset + prefix] = element_in; g_indices_out[binOffset + prefix] = index_in; if (prefix == count - 1) { atomicAdd(global_offsets[binID], count); } } barrier(); } } ================================================ FILE: shader/multi_radixsort_histograms.glsl ================================================ /** * VkRadixSort written by Mirco Werner: https://github.com/MircoWerner/VkRadixSort * Based on implementation of Intel's Embree: https://github.com/embree/embree/blob/v4.0.0-ploc/kernels/rthwif/builder/gpu/sort.h */ #version 460 //#extension GL_GOOGLE_include_directive: enable #define WORKGROUP_SIZE 256 // assert WORKGROUP_SIZE >= RADIX_SORT_BINS #define RADIX_SORT_BINS 256 uniform uint g_num_elements; uniform uint g_shift; //uniform uint g_num_workgroups; uniform uint g_num_blocks_per_workgroup; layout (local_size_x = WORKGROUP_SIZE) in; layout (std430, binding = 0) buffer elements_in { uint g_elements_in[]; }; layout (std430, binding = 1) buffer histograms { // [histogram_of_workgroup_0 | histogram_of_workgroup_1 | ... ] uint g_histograms[]; // |g_histograms| = RADIX_SORT_BINS * #WORKGROUPS }; shared uint[RADIX_SORT_BINS] histogram; void main() { uint gID = gl_GlobalInvocationID.x; uint lID = gl_LocalInvocationID.x; uint wID = gl_WorkGroupID.x; // initialize histogram if (lID < RADIX_SORT_BINS) { histogram[lID] = 0U; } barrier(); for (uint index = 0; index < g_num_blocks_per_workgroup; index++) { uint elementId = wID * g_num_blocks_per_workgroup * WORKGROUP_SIZE + index * WORKGROUP_SIZE + lID; if (elementId < g_num_elements) { // determine the bin const uint bin = (g_elements_in[elementId] >> g_shift) & (RADIX_SORT_BINS - 1); // increment the histogram atomicAdd(histogram[bin], 1U); } } barrier(); if (lID < RADIX_SORT_BINS) { g_histograms[RADIX_SORT_BINS * wID + lID] = histogram[lID]; } } ================================================ FILE: shader/point_frag.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // // fullbright textured particle // /*%%HEADER%%*/ uniform sampler2D colorTex; in vec2 frag_uv; in vec4 frag_color; out vec4 out_color; void main() { vec4 texColor = texture(colorTex, frag_uv); // premultiplied alpha blending out_color.rgb = frag_color.a * frag_color.rgb * texColor.rgb; out_color.a = frag_color.a * texColor.a; } ================================================ FILE: shader/point_geom.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ /*%%HEADER%%*/ uniform float pointSize; uniform float invAspectRatio; layout(points) in; layout(triangle_strip, max_vertices = 4) out; in vec4 geom_color[]; out vec4 frag_color; out vec2 frag_uv; void main() { vec2 offset = vec2(pointSize * invAspectRatio, pointSize); // bottom-left vertex frag_uv = vec2(0.0, 0.0); frag_color = geom_color[0]; gl_Position = gl_in[0].gl_Position + vec4(-offset.x, -offset.y, 0.0, 0.0); EmitVertex(); // bottom-right vertex frag_uv = vec2(1.0, 0.0); frag_color = geom_color[0]; gl_Position = gl_in[0].gl_Position + vec4(offset.x, -offset.y, 0.0, 0.0); EmitVertex(); // top-left vertex frag_uv = vec2(0.0, 1.0); frag_color = geom_color[0]; gl_Position = gl_in[0].gl_Position + vec4(-offset.x, offset.y, 0.0, 0.0); EmitVertex(); // top-right vertex frag_uv = vec2(1.0, 1.0); frag_color = geom_color[0]; gl_Position = gl_in[0].gl_Position + vec4(offset.x, offset.y, 0.0, 0.0); EmitVertex(); EndPrimitive(); } ================================================ FILE: shader/point_vert.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // // fullbright textured particle // /*%%HEADER%%*/ uniform float pointSize; uniform float invAspectRatio; uniform mat4 modelViewMat; uniform mat4 projMat; in vec4 position; in vec4 color; out vec4 geom_color; void main(void) { gl_Position = projMat * modelViewMat * position; geom_color = color; } ================================================ FILE: shader/presort_compute.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ /*%%HEADER%%*/ layout(local_size_x = 256) in; uniform mat4 modelViewProj; uniform vec2 nearFar; uniform uint keyMax; layout(binding = 4, offset = 0) uniform atomic_uint output_count; layout(std430, binding = 0) readonly buffer PosBuffer { vec4 positions[]; }; layout(std430, binding = 1) writeonly buffer OutputBuffer { uint quantizedZs[]; }; layout(std430, binding = 2) writeonly buffer OutputBuffer2 { uint indices[]; }; void main() { uint idx = gl_GlobalInvocationID.x; uint len = uint(positions.length()); if (idx >= len) { return; } // NOTE: alpha is encoded into the w component of the positions vec4 p = modelViewProj * vec4(positions[idx].xyz, 1.0f); float depth = p.w; float xx = p.x / depth; float yy = p.y / depth; const float CLIP = 1.5f; if (depth > 0.0f && xx < CLIP && xx > -CLIP && yy < CLIP && yy > -CLIP) { uint count = atomicCounterIncrement(output_count); // 16.16 fixed point //uint fixedPointZ = uint(0xffffffff) - uint(clamp(depth, 0.0f, 65535.0f) * 65536.0f); uint fixedPointZ = keyMax - uint((depth / nearFar.y) * keyMax); quantizedZs[count] = fixedPointZ; indices[count] = idx; } } ================================================ FILE: shader/single_radixsort.glsl ================================================ /** * VkRadixSort written by Mirco Werner: https://github.com/MircoWerner/VkRadixSort * Based on implementation of Intel's Embree: https://github.com/embree/embree/blob/v4.0.0-ploc/kernels/rthwif/builder/gpu/sort.h */ #version 460 #extension GL_KHR_shader_subgroup_basic: enable #extension GL_KHR_shader_subgroup_arithmetic: enable #define WORKGROUP_SIZE 256// assert WORKGROUP_SIZE >= RADIX_SORT_BINS #define RADIX_SORT_BINS 256 #define SUBGROUP_SIZE 32// 32 NVIDIA; 64 AMD #define BITS 32// sorting uint32_t #define ITERATIONS 4// 4 iterations, sorting 8 bits per iteration layout (local_size_x = WORKGROUP_SIZE) in; uniform uint g_num_elements; layout (std430, set = 0, binding = 0) buffer elements_in { uint g_elements_in[]; }; layout (std430, set = 0, binding = 1) buffer elements_out { uint g_elements_out[]; }; layout (std430, set = 0, binding = 2) buffer indices_in { uint g_indices_in[]; }; layout (std430, set = 0, binding = 3) buffer indices_out { uint g_indices_out[]; }; shared uint[RADIX_SORT_BINS] histogram; shared uint[RADIX_SORT_BINS / SUBGROUP_SIZE] sums;// subgroup reductions shared uint[RADIX_SORT_BINS] local_offsets;// local exclusive scan (prefix sum) (inside subgroups) shared uint[RADIX_SORT_BINS] global_offsets;// global exclusive scan (prefix sum) struct BinFlags { uint flags[WORKGROUP_SIZE / BITS]; }; shared BinFlags[RADIX_SORT_BINS] bin_flags; #define ELEMENT_IN(index, iteration) (iteration % 2 == 0 ? g_elements_in[index] : g_elements_out[index]) #define INDEX_IN(index, iteration) (iteration % 2 == 0 ? g_indices_in[index] : g_indices_out[index]) void main() { uint lID = gl_LocalInvocationID.x; uint sID = gl_SubgroupID; uint lsID = gl_SubgroupInvocationID; for (uint iteration = 0; iteration < ITERATIONS; iteration++) { uint shift = 8 * iteration; // initialize histogram if (lID < RADIX_SORT_BINS) { histogram[lID] = 0U; } barrier(); for (uint ID = lID; ID < g_num_elements; ID += WORKGROUP_SIZE) { // determine the bin const uint bin = (ELEMENT_IN(ID, iteration) >> shift) & (RADIX_SORT_BINS - 1); // increment the histogram atomicAdd(histogram[bin], 1U); } barrier(); // subgroup reductions and subgroup prefix sums if (lID < RADIX_SORT_BINS) { uint histogram_count = histogram[lID]; uint sum = subgroupAdd(histogram_count); uint prefix_sum = subgroupExclusiveAdd(histogram_count); local_offsets[lID] = prefix_sum; if (subgroupElect()) { // one thread inside the warp/subgroup enters this section sums[sID] = sum; } } barrier(); // global prefix sums (offsets) if (sID == 0) { uint offset = 0; for (uint i = lsID; i < RADIX_SORT_BINS; i += SUBGROUP_SIZE) { global_offsets[i] = offset + local_offsets[i]; offset += sums[i / SUBGROUP_SIZE]; } } barrier(); // ==== scatter keys according to global offsets ===== const uint flags_bin = lID / BITS; const uint flags_bit = 1 << (lID % BITS); for (uint blockID = 0; blockID < g_num_elements; blockID += WORKGROUP_SIZE) { barrier(); const uint ID = blockID + lID; // initialize bin flags if (lID < RADIX_SORT_BINS) { for (int i = 0; i < WORKGROUP_SIZE / BITS; i++) { bin_flags[lID].flags[i] = 0U;// init all bin flags to 0 } } barrier(); uint element_in = 0; uint index_in = 0; uint binID = 0; uint binOffset = 0; if (ID < g_num_elements) { element_in = ELEMENT_IN(ID, iteration); index_in = INDEX_IN(ID, iteration); binID = (element_in >> shift) & uint(RADIX_SORT_BINS - 1); // offset for group binOffset = global_offsets[binID]; // add bit to flag atomicAdd(bin_flags[binID].flags[flags_bin], flags_bit); } barrier(); if (ID < g_num_elements) { // calculate output index of element uint prefix = 0; uint count = 0; for (uint i = 0; i < WORKGROUP_SIZE / BITS; i++) { const uint bits = bin_flags[binID].flags[i]; const uint full_count = bitCount(bits); const uint partial_count = bitCount(bits & (flags_bit - 1)); prefix += (i < flags_bin) ? full_count : 0U; prefix += (i == flags_bin) ? partial_count : 0U; count += full_count; } if (iteration % 2 == 0) { g_elements_out[binOffset + prefix] = element_in; g_indices_out[binOffset + prefix] = index_in; } else { g_elements_in[binOffset + prefix] = element_in; g_indices_in[binOffset + prefix] = index_in; } if (prefix == count - 1) { atomicAdd(global_offsets[binID], count); } } } } } ================================================ FILE: shader/splat_frag.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // // 3d gaussian splat fragment shader // /*%%HEADER%%*/ in vec4 frag_color; // radiance of splat in vec4 frag_cov2inv; // inverse of the 2D screen space covariance matrix of the guassian in vec2 frag_p; // 2D screen space center of the guassian out vec4 out_color; void main() { vec2 d = gl_FragCoord.xy - frag_p; // TODO: Use texture for gaussian evaluation // evaluate the gaussian mat2 cov2Dinv = mat2(frag_cov2inv.xy, frag_cov2inv.zw); float g = exp(-0.5f * dot(d, cov2Dinv * d)); out_color.rgb = frag_color.a * g * frag_color.rgb; out_color.a = frag_color.a * g; /* // can be used to determine overdraw. float epsilon = 1.0f / 256.0f; out_color.rgb = frag_color.a * g * frag_color.rgb; out_color.rgb = frag_color.rgb * 0.00000001f + vec3(epsilon, epsilon, epsilon); out_color.a = 0.0f; */ if ((frag_color.a * g) <= (1.0f / 256.0f)) { discard; } } ================================================ FILE: shader/splat_geom.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ /*%%HEADER%%*/ uniform vec4 viewport; // x, y, WIDTH, HEIGHT layout(points) in; layout(triangle_strip, max_vertices = 4) out; in vec4 geom_color[]; // radiance of splat in vec4 geom_cov2[]; // 2D screen space covariance matrix of the gaussian in vec2 geom_p[]; // the 2D screen space center of the gaussian out vec4 frag_color; // radiance of splat out vec4 frag_cov2inv; // inverse of the 2D screen space covariance matrix of the guassian out vec2 frag_p; // the 2D screen space center of the gaussian // used to invert the 2D screen space covariance matrix mat2 inverseMat2(mat2 m) { float det = m[0][0] * m[1][1] - m[0][1] * m[1][0]; mat2 inv; inv[0][0] = m[1][1] / det; inv[0][1] = -m[0][1] / det; inv[1][0] = -m[1][0] / det; inv[1][1] = m[0][0] / det; return inv; } void main() { float WIDTH = viewport.z; float HEIGHT = viewport.w; mat2 cov2D = mat2(geom_cov2[0].xy, geom_cov2[0].zw); // we pass the inverse of the 2d covariance matrix to the pixel shader, to avoid doing a matrix inverse per pixel. mat2 cov2Dinv = inverseMat2(cov2D); vec4 cov2Dinv4 = vec4(cov2Dinv[0], cov2Dinv[1]); // cram it into a vec4 // discard splats that end up outside of a guard band vec4 p4 = gl_in[0].gl_Position; vec3 ndcP = p4.xyz / p4.w; if (ndcP.z < 0.25f || ndcP.x > 2.0f || ndcP.x < -2.0f || ndcP.y > 2.0f || ndcP.y < -2.0f) { // discard this point return; } // compute 2d extents for the splat, using covariance matrix ellipse // see https://cookierobotics.com/007/ float k = 3.5f; float a = cov2D[0][0]; float b = cov2D[0][1]; float c = cov2D[1][1]; float apco2 = (a + c) / 2.0f; float amco2 = (a - c) / 2.0f; float term = sqrt(amco2 * amco2 + b * b); float maj = apco2 + term; float min = apco2 - term; float theta; if (b == 0.0f) { theta = (a >= c) ? 0.0f : radians(90.0f); } else { theta = atan(maj - a, b); } float r1 = k * sqrt(maj); float r2 = k * sqrt(min); vec2 majAxis = vec2(r1 * cos(theta), r1 * sin(theta)); vec2 minAxis = vec2(r2 * cos(theta + radians(90.0f)), r2 * sin(theta + radians(90.0f))); vec2 offsets[4]; offsets[0] = majAxis + minAxis; offsets[1] = -majAxis + minAxis; offsets[3] = -majAxis - minAxis; offsets[2] = majAxis - minAxis; vec2 offset; float w = gl_in[0].gl_Position.w; for (int i = 0; i < 4; i++) { // transform offset back into clip space, and apply it to gl_Position. offset = offsets[i]; offset.x *= (2.0f / WIDTH) * w; offset.y *= (2.0f / HEIGHT) * w; gl_Position = gl_in[0].gl_Position + vec4(offset.x, offset.y, 0.0, 0.0); frag_color = geom_color[0]; frag_cov2inv = cov2Dinv4; frag_p = geom_p[0]; EmitVertex(); } EndPrimitive(); } ================================================ FILE: shader/splat_peel_frag.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // // 3d gaussian splat fragment shader // /*%%HEADER%%*/ uniform sampler2D depthTex; uniform vec4 viewport; // x, y, WIDTH, HEIGHT in vec4 frag_color; // radiance of splat in vec4 frag_cov2inv; // inverse of the 2D screen space covariance matrix of the guassian in vec2 frag_p; // 2D screen space center of the guassian out vec4 out_color; void main() { vec2 d = gl_FragCoord.xy - frag_p; vec2 uv = gl_FragCoord.xy / viewport.zw; float depth = texture(depthTex, uv).r; if (gl_FragCoord.z <= depth) { discard; } // TODO: Use texture for gaussian evaluation // evaluate the gaussian mat2 cov2Dinv = mat2(frag_cov2inv.xy, frag_cov2inv.zw); float g = exp(-0.5f * dot(d, cov2Dinv * d)); out_color.rgb = frag_color.a * g * frag_color.rgb; out_color.a = frag_color.a * g; if ((frag_color.a * g) <= (10.0f / 256.0f)) { discard; } /* // can be used to determine overdraw. float epsilon = 1.0f / 256.0f; out_color.rgb = frag_color.a * g * frag_color.rgb; out_color.rgb = frag_color.rgb * 0.00000001f + vec3(epsilon, epsilon, epsilon); out_color.a = 0.0f; */ } ================================================ FILE: shader/splat_vert.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // // 3d gaussian splat vertex shader // /*%%HEADER%%*/ /*%%DEFINES%%*/ uniform mat4 viewMat; // used to project position into view coordinates. uniform mat4 projMat; // used to project view coordinates into clip coordinates. uniform vec4 projParams; // x = HEIGHT / tan(FOVY / 2), y = Z_NEAR, z = Z_FAR uniform vec4 viewport; // x, y, WIDTH, HEIGHT uniform vec3 eye; in vec4 position; // center of the gaussian in object coordinates, (with alpha crammed in to w) // spherical harmonics coeff for radiance of the splat in vec4 r_sh0; // sh coeff for red channel (up to third-order) #ifdef FULL_SH in vec4 r_sh1; in vec4 r_sh2; in vec4 r_sh3; #endif in vec4 g_sh0; // sh coeff for green channel #ifdef FULL_SH in vec4 g_sh1; in vec4 g_sh2; in vec4 g_sh3; #endif in vec4 b_sh0; // sh coeff for blue channel #ifdef FULL_SH in vec4 b_sh1; in vec4 b_sh2; in vec4 b_sh3; #endif // 3x3 covariance matrix of the splat in object coordinates. in vec3 cov3_col0; in vec3 cov3_col1; in vec3 cov3_col2; out vec4 geom_color; // radiance of splat out vec4 geom_cov2; // 2D screen space covariance matrix of the gaussian out vec2 geom_p; // the 2D screen space center of the gaussian, (z is alpha) vec3 ComputeRadianceFromSH(const vec3 v) { #ifdef FULL_SH float b[16]; #else float b[4]; #endif float vx2 = v.x * v.x; float vy2 = v.y * v.y; float vz2 = v.z * v.z; // zeroth order // (/ 1.0 (* 2.0 (sqrt pi))) b[0] = 0.28209479177387814f; // first order // (/ (sqrt 3.0) (* 2 (sqrt pi))) float k1 = 0.4886025119029199f; b[1] = -k1 * v.y; b[2] = k1 * v.z; b[3] = -k1 * v.x; #ifdef FULL_SH // second order // (/ (sqrt 15.0) (* 2 (sqrt pi))) float k2 = 1.0925484305920792f; // (/ (sqrt 5.0) (* 4 (sqrt pi))) float k3 = 0.31539156525252005f; // (/ (sqrt 15.0) (* 4 (sqrt pi))) float k4 = 0.5462742152960396f; b[4] = k2 * v.y * v.x; b[5] = -k2 * v.y * v.z; b[6] = k3 * (3.0f * vz2 - 1.0f); b[7] = -k2 * v.x * v.z; b[8] = k4 * (vx2 - vy2); // third order // (/ (* (sqrt 2) (sqrt 35)) (* 8 (sqrt pi))) float k5 = 0.5900435899266435f; // (/ (sqrt 105) (* 2 (sqrt pi))) float k6 = 2.8906114426405543f; // (/ (* (sqrt 2) (sqrt 21)) (* 8 (sqrt pi))) float k7 = 0.4570457994644658f; // (/ (sqrt 7) (* 4 (sqrt pi))) float k8 = 0.37317633259011546f; // (/ (sqrt 105) (* 4 (sqrt pi))) float k9 = 1.4453057213202771f; b[9] = -k5 * v.y * (3.0f * vx2 - vy2); b[10] = k6 * v.y * v.x * v.z; b[11] = -k7 * v.y * (5.0f * vz2 - 1.0f); b[12] = k8 * v.z * (5.0f * vz2 - 3.0f); b[13] = -k7 * v.x * (5.0f * vz2 - 1.0f); b[14] = k9 * v.z * (vx2 - vy2); b[15] = -k5 * v.x * (vx2 - 3.0f * vy2); float re = (b[0] * r_sh0.x + b[1] * r_sh0.y + b[2] * r_sh0.z + b[3] * r_sh0.w + b[4] * r_sh1.x + b[5] * r_sh1.y + b[6] * r_sh1.z + b[7] * r_sh1.w + b[8] * r_sh2.x + b[9] * r_sh2.y + b[10]* r_sh2.z + b[11]* r_sh2.w + b[12]* r_sh3.x + b[13]* r_sh3.y + b[14]* r_sh3.z + b[15]* r_sh3.w); float gr = (b[0] * g_sh0.x + b[1] * g_sh0.y + b[2] * g_sh0.z + b[3] * g_sh0.w + b[4] * g_sh1.x + b[5] * g_sh1.y + b[6] * g_sh1.z + b[7] * g_sh1.w + b[8] * g_sh2.x + b[9] * g_sh2.y + b[10]* g_sh2.z + b[11]* g_sh2.w + b[12]* g_sh3.x + b[13]* g_sh3.y + b[14]* g_sh3.z + b[15]* g_sh3.w); float bl = (b[0] * b_sh0.x + b[1] * b_sh0.y + b[2] * b_sh0.z + b[3] * b_sh0.w + b[4] * b_sh1.x + b[5] * b_sh1.y + b[6] * b_sh1.z + b[7] * b_sh1.w + b[8] * b_sh2.x + b[9] * b_sh2.y + b[10]* b_sh2.z + b[11]* b_sh2.w + b[12]* b_sh3.x + b[13]* b_sh3.y + b[14]* b_sh3.z + b[15]* b_sh3.w); #else float re = (b[0] * r_sh0.x + b[1] * r_sh0.y + b[2] * r_sh0.z + b[3] * r_sh0.w); float gr = (b[0] * g_sh0.x + b[1] * g_sh0.y + b[2] * g_sh0.z + b[3] * g_sh0.w); float bl = (b[0] * b_sh0.x + b[1] * b_sh0.y + b[2] * b_sh0.z + b[3] * b_sh0.w); #endif return vec3(0.5f, 0.5f, 0.5f) + vec3(re, gr, bl); } #ifdef FRAMEBUFFER_SRGB float SRGBToLinearF(float srgb) { if (srgb <= 0.04045f) { return srgb / 12.92f; } else { return pow((srgb + 0.055f) / 1.055f, 2.4f); } } vec3 SRGBToLinear(const vec3 srgbColor) { vec3 linearColor; for (int i = 0; i < 3; ++i) // Convert RGB, leave A unchanged { linearColor[i] = SRGBToLinearF(srgbColor[i]); } return linearColor; } #endif void main(void) { // t is in view coordinates float alpha = position.w; vec4 t = viewMat * vec4(position.xyz, 1.0f); //float X0 = viewport.x; float X0 = viewport.x * (0.00001f * projParams.y); // one weird hack to prevent projParams from being compiled away float Y0 = viewport.y; float WIDTH = viewport.z; float HEIGHT = viewport.w; float Z_NEAR = projParams.y; float Z_FAR = projParams.z; // J is the jacobian of the projection and viewport transformations. // this is an affine approximation of the real projection. // because gaussians are closed under affine transforms. float SX = projMat[0][0]; float SY = projMat[1][1]; float WZ = projMat[3][2]; float tzSq = t.z * t.z; float jsx = -(SX * WIDTH) / (2.0f * t.z); float jsy = -(SY * HEIGHT) / (2.0f * t.z); float jtx = (SX * t.x * WIDTH) / (2.0f * tzSq); float jty = (SY * t.y * HEIGHT) / (2.0f * tzSq); float jtz = ((Z_FAR - Z_NEAR) * WZ) / (2.0f * tzSq); mat3 J = mat3(vec3(jsx, 0.0f, 0.0f), vec3(0.0f, jsy, 0.0f), vec3(jtx, jty, jtz)); // combine the affine transforms of W (viewMat) and J (approx of viewportMat * projMat) // using the fact that the new transformed covariance matrix V_Prime = JW * V * (JW)^T mat3 W = mat3(viewMat); mat3 V = mat3(cov3_col0, cov3_col1, cov3_col2); mat3 JW = J * W; mat3 V_prime = JW * V * transpose(JW); // now we can 'project' the 3D covariance matrix onto the xy plane by just dropping the last column and row. mat2 cov2D = mat2(V_prime); // use the fact that the convolution of a gaussian with another gaussian is the sum // of their covariance matrices to apply a low-pass filter to anti-alias the splats cov2D[0][0] += 0.3f; cov2D[1][1] += 0.3f; geom_cov2 = vec4(cov2D[0], cov2D[1]); // cram it into a vec4 // geom_p is the gaussian center transformed into screen space vec4 p4 = projMat * t; geom_p = vec2(p4.x / p4.w, p4.y / p4.w); geom_p.x = 0.5f * (WIDTH + (geom_p.x * WIDTH) + (2.0f * X0)); geom_p.y = 0.5f * (HEIGHT + (geom_p.y * HEIGHT) + (2.0f * Y0)); // compute radiance from sh vec3 v = normalize(position.xyz - eye); geom_color = vec4(ComputeRadianceFromSH(v), alpha); #ifdef FRAMEBUFFER_SRGB // The SIBR reference renderer uses sRGB throughout, // i.e. the splat colors are sRGB, the gaussian and alpha-blending occurs in sRGB space. // However, in vr our shader output must be in linear space, // in order for openxr color conversion to work. // So, we convert the splat color to linear, // but the guassian and alpha-blending occur in linear space. // This leads to results that don't quite match the SIBR reference. geom_color.rgb = SRGBToLinear(geom_color.rgb); #endif // gl_Position is in clip coordinates. gl_Position = p4; } ================================================ FILE: shader/text_frag.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ /*%%HEADER%%*/ uniform sampler2D fontTex; in vec2 frag_uv; in vec4 frag_color; out vec4 out_color; void main() { vec4 texColor = texture(fontTex, frag_uv, -0.5f); // bias to increase sharpness a bit // premultiplied alpha blending out_color.rgb = frag_color.a * frag_color.rgb * texColor.rgb; out_color.a = frag_color.a * texColor.a; } ================================================ FILE: shader/text_vert.glsl ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ /*%%HEADER%%*/ uniform mat4 modelViewProjMat; in vec3 position; in vec2 uv; in vec4 color; out vec2 frag_uv; out vec4 frag_color; void main(void) { gl_Position = modelViewProjMat * vec4(position, 1.0f); frag_uv = uv; frag_color = color; } ================================================ FILE: src/android_main.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include // for native window JNI #include #include #include #include #include #include #include // for prctl( PR_SET_NAME ) #include #include #include "core/log.h" #include "core/util.h" #include "core/xrbuddy.h" #include "app.h" // AJT: ANDROID TODO: xrPerfSettingsSetPerformanceLevelEXT // AJT: ANDROID TODO: pfnSetAndroidApplicationThreadKHR on XR_SESSION_STATE_READY // see ovrApp::HandleSessionStateChanges in SceneModelXr.cpp /* static const int CPU_LEVEL = 2; static const int GPU_LEVEL = 3; */ static const char* EglErrorString(const EGLint error) { switch (error) { case EGL_SUCCESS: return "EGL_SUCCESS"; case EGL_NOT_INITIALIZED: return "EGL_NOT_INITIALIZED"; case EGL_BAD_ACCESS: return "EGL_BAD_ACCESS"; case EGL_BAD_ALLOC: return "EGL_BAD_ALLOC"; case EGL_BAD_ATTRIBUTE: return "EGL_BAD_ATTRIBUTE"; case EGL_BAD_CONTEXT: return "EGL_BAD_CONTEXT"; case EGL_BAD_CONFIG: return "EGL_BAD_CONFIG"; case EGL_BAD_CURRENT_SURFACE: return "EGL_BAD_CURRENT_SURFACE"; case EGL_BAD_DISPLAY: return "EGL_BAD_DISPLAY"; case EGL_BAD_SURFACE: return "EGL_BAD_SURFACE"; case EGL_BAD_MATCH: return "EGL_BAD_MATCH"; case EGL_BAD_PARAMETER: return "EGL_BAD_PARAMETER"; case EGL_BAD_NATIVE_PIXMAP: return "EGL_BAD_NATIVE_PIXMAP"; case EGL_BAD_NATIVE_WINDOW: return "EGL_BAD_NATIVE_WINDOW"; case EGL_CONTEXT_LOST: return "EGL_CONTEXT_LOST"; default: return "unknown"; } } struct AppContext { AppContext() : resumed(false), sessionActive(false), assMan(nullptr), alwaysCopyAssets(true) {} bool resumed; bool sessionActive; struct EGLInfo { EGLInfo() : majorVersion(0), minorVersion(0), display(0), config(0), context(EGL_NO_CONTEXT) {} EGLint majorVersion; EGLint minorVersion; EGLDisplay display; EGLConfig config; EGLContext context; EGLSurface tinySurface; }; EGLInfo egl; AAssetManager* assMan; std::string externalDataPath; bool alwaysCopyAssets; void Clear() { resumed = false; sessionActive = false; egl.majorVersion = 0; egl.minorVersion = 0; egl.display = 0; egl.config = 0; egl.context = EGL_NO_CONTEXT; } bool SetupEGLContext() { // create the egl context egl.display = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(egl.display, &egl.majorVersion, &egl.minorVersion); Log::D("OpenGLES majorVersion = %d, minorVersion = %d\n", egl.majorVersion, egl.minorVersion); const int MAX_CONFIGS = 1024; EGLConfig configs[MAX_CONFIGS]; EGLint numConfigs = 0; if (!eglGetConfigs(egl.display, configs, MAX_CONFIGS, &numConfigs)) { Log::E("eglGetConfigs failed: %s\n", EglErrorString(eglGetError())); return false; } const EGLint configAttribs[] = {EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, // need alpha for the multi-pass timewarp compositor EGL_DEPTH_SIZE, 0, EGL_STENCIL_SIZE, 0, EGL_SAMPLES, 0, EGL_NONE}; egl.config = 0; for (int i = 0; i < numConfigs; i++) { EGLint value = 0; eglGetConfigAttrib(egl.display, configs[i], EGL_RENDERABLE_TYPE, &value); if ((value & EGL_OPENGL_ES3_BIT_KHR) != EGL_OPENGL_ES3_BIT_KHR) { continue; } // The pbuffer config also needs to be compatible with normal window rendering // so it can share textures with the window context. eglGetConfigAttrib(egl.display, configs[i], EGL_SURFACE_TYPE, &value); if ((value & (EGL_WINDOW_BIT | EGL_PBUFFER_BIT)) != (EGL_WINDOW_BIT | EGL_PBUFFER_BIT)) { continue; } int j = 0; for (; configAttribs[j] != EGL_NONE; j += 2) { eglGetConfigAttrib(egl.display, configs[i], configAttribs[j], &value); if (value != configAttribs[j + 1]) { break; } } if (configAttribs[j] == EGL_NONE) { egl.config = configs[i]; break; } } if (egl.config == 0) { Log::E("eglChooseConfig() failed: %s\n", EglErrorString(eglGetError())); return false; } EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; egl.context = eglCreateContext(egl.display, egl.config, EGL_NO_CONTEXT, contextAttribs); if (egl.context == EGL_NO_CONTEXT) { Log::E("eglCreateContext() failed: %s", EglErrorString(eglGetError())); return false; } const EGLint surfaceAttribs[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE}; egl.tinySurface = eglCreatePbufferSurface(egl.display, egl.config, surfaceAttribs); if (egl.tinySurface == EGL_NO_SURFACE) { Log::E("eglCreatePbufferSurface() failed: %s", EglErrorString(eglGetError())); eglDestroyContext(egl.display, egl.context); egl.context = EGL_NO_CONTEXT; return false; } if (eglMakeCurrent(egl.display, egl.tinySurface, egl.tinySurface, egl.context) == EGL_FALSE) { Log::E("eglMakeCurrent() failed: %s", EglErrorString(eglGetError())); eglDestroySurface(egl.display, egl.tinySurface); eglDestroyContext(egl.display, egl.context); egl.context = EGL_NO_CONTEXT; return false; } return true; } bool SetupAssets(android_app* app) { assert(app); assert(app->activity->assetManager); assert(app->activity->externalDataPath); assMan = app->activity->assetManager; externalDataPath = std::string(app->activity->externalDataPath) + "/"; // from util.h SetRootPath(externalDataPath); Log::D("AJT: externalDataPath = \"%s\"\n", externalDataPath.c_str()); MakeDir("texture"); UnpackAsset("texture/carpet.png"); UnpackAsset("texture/sphere.png"); MakeDir("shader"); UnpackAsset("shader/carpet_frag.glsl"); UnpackAsset("shader/carpet_vert.glsl"); UnpackAsset("shader/debugdraw_frag.glsl"); UnpackAsset("shader/debugdraw_vert.glsl"); UnpackAsset("shader/desktop_frag.glsl"); UnpackAsset("shader/desktop_vert.glsl"); UnpackAsset("shader/point_frag.glsl"); UnpackAsset("shader/point_geom.glsl"); UnpackAsset("shader/point_vert.glsl"); UnpackAsset("shader/presort_compute.glsl"); UnpackAsset("shader/splat_frag.glsl"); UnpackAsset("shader/splat_geom.glsl"); UnpackAsset("shader/splat_vert.glsl"); UnpackAsset("shader/text_frag.glsl"); UnpackAsset("shader/text_vert.glsl"); MakeDir("font"); UnpackAsset("font/JetBrainsMono-Medium.json"); UnpackAsset("font/JetBrainsMono-Medium.png"); MakeDir("data"); MakeDir("data/sh_test"); UnpackAsset("data/sh_test/cameras.json"); UnpackAsset("data/sh_test/cfg_args"); UnpackAsset("data/sh_test/input.ply"); MakeDir("data/sh_test/point_cloud"); MakeDir("data/sh_test/point_cloud/iteration_30000"); UnpackAsset("data/sh_test/point_cloud/iteration_30000/point_cloud.ply"); UnpackAsset("data/sh_test/vr.json"); MakeDir("data/livingroom"); UnpackAsset("data/livingroom/livingroom.ply"); UnpackAsset("data/livingroom/livingroom_vr.json"); return true; } bool MakeDir(const std::string& dirFilename) { std::string filename = externalDataPath + dirFilename; if (mkdir(filename.c_str(), 0777) != 0) { if (errno == EEXIST) { Log::D("MakeDir \"%s\" already exists\n", dirFilename.c_str()); // dir already exists! return true; } else { Log::E("mkdir failed on dir \"%s\" errno = %d\n", filename.c_str(), errno); return false; } } Log::D("MakeDir \"%s\"\n", dirFilename.c_str()); return true; } bool UnpackAsset(const std::string& assetFilename) { std::string outputFilename = externalDataPath + assetFilename; struct stat sb; if (stat(outputFilename.c_str(), &sb) == 0) { if (!alwaysCopyAssets) { Log::D("UnpackAsset \"%s\" already exists\n", assetFilename.c_str()); return true; } } AAsset *asset = AAssetManager_open(assMan, assetFilename.c_str(), AASSET_MODE_STREAMING); if (asset == nullptr) { Log::E("UnpackAsset \"%s\" AAssetManager_open failed!\n", assetFilename.c_str()); return false; // Failed to open the asset } // Create buffer for reading const size_t BUFFER_SIZE = 1024; char buffer[BUFFER_SIZE]; // Open file for writing FILE *outFile = fopen(outputFilename.c_str(), "w"); if (outFile == nullptr) { Log::E("UnpackAsset \"%s\" fopen failed!\n", assetFilename.c_str()); AAsset_close(asset); return false; // Failed to open the output file } // Read from assets and write to file int bytesRead; while ((bytesRead = AAsset_read(asset, buffer, BUFFER_SIZE)) > 0) { fwrite(buffer, sizeof(char), bytesRead, outFile); } // Close the asset and the output file AAsset_close(asset); fclose(outFile); Log::D("UnpackAsset \"%s\"\n", assetFilename.c_str()); return true; } }; /** * Process the next main command. */ static void app_handle_cmd(struct android_app* androidApp, int32_t cmd) { AppContext& ctx = *(AppContext*)androidApp->userData; switch (cmd) { // There is no APP_CMD_CREATE. The ANativeActivity creates the // application thread from onCreate(). The application thread // then calls android_main(). case APP_CMD_START: Log::D("onStart()\n"); Log::D(" APP_CMD_START\n"); break; case APP_CMD_RESUME: Log::D("onResume()\n"); Log::D(" APP_CMD_RESUME\n"); ctx.resumed = true; break; case APP_CMD_PAUSE: Log::D("onPause()\n"); Log::D(" APP_CMD_PAUSE\n"); ctx.resumed = false; break; case APP_CMD_STOP: Log::D("onStop()\n"); Log::D(" APP_CMD_STOP\n"); break; case APP_CMD_DESTROY: Log::D("onDestroy()\n"); Log::D(" APP_CMD_DESTROY\n"); ctx.Clear(); break; case APP_CMD_INIT_WINDOW: Log::D("surfaceCreated()\n"); Log::D(" APP_CMD_INIT_WINDOW\n"); break; case APP_CMD_TERM_WINDOW: Log::D("surfaceDestroyed()\n"); Log::D(" APP_CMD_TERM_WINDOW\n"); break; } } /** * This is the main entry point of a native application that is using * android_native_app_glue. It runs in its own thread, with its own * event loop for receiving input events and doing other things. */ void android_main(struct android_app* androidApp) { Log::SetAppName("splatapult"); Log::D("----------------------------------------------------------------\n"); Log::D("android_app_entry()\n"); Log::D(" android_main()\n"); JNIEnv* env; (*androidApp->activity->vm).AttachCurrentThread(&env, nullptr); // Note that AttachCurrentThread will reset the thread name. prctl(PR_SET_NAME, (long)"android_main", 0, 0, 0); AppContext ctx; androidApp->userData = &ctx; androidApp->onAppCmd = app_handle_cmd; if (!ctx.SetupEGLContext()) { Log::E("AppContext::SetupEGLContext failed!\n"); return; } if (!ctx.SetupAssets(androidApp)) { Log::E("AppContext::SetupAssets failed!\n"); return; } MainContext mainContext; mainContext.display = ctx.egl.display; mainContext.config = ctx.egl.config; mainContext.context = ctx.egl.context; mainContext.androidApp = androidApp; std::string dataPath = ctx.externalDataPath + "data/livingroom/livingroom.ply"; int argc = 4; const char* argv[] = {"splataplut", "-v", "-d", dataPath.c_str()}; App app(mainContext); App::ParseResult parseResult = app.ParseArguments(argc, argv); switch (parseResult) { case App::SUCCESS_RESULT: break; case App::ERROR_RESULT: Log::E("App::ParseArguments failed!\n"); return; case App::QUIT_RESULT: return; } if (!app.Init()) { Log::E("App::Init failed!\n"); return; } while (androidApp->destroyRequested == 0) { // Read all pending events. for (;;) { int events; struct android_poll_source* source; // If the timeout is zero, returns immediately without blocking. // If the timeout is negative, waits indefinitely until an event appears. int timeoutMilliseconds = 0; if (ctx.resumed == false && ctx.sessionActive == false && androidApp->destroyRequested == 0) { timeoutMilliseconds = -1; } if (ALooper_pollAll(timeoutMilliseconds, NULL, &events, (void**)&source) < 0) { break; } // Process this event. if (source != NULL) { source->process(androidApp, source); } } float dt = 1.0f / 72.0f; if (!app.Process(dt)) { Log::E("App::Process failed!\n"); break; } if (!app.Render(dt, glm::ivec2(0.0f, 0.0f))) { Log::E("App::Render failed!\n"); return; } } // TODO: DESTROY STUFF Log::D("Finished!\n"); (*androidApp->activity->vm).DetachCurrentThread(); } ================================================ FILE: src/app.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "app.h" #ifndef __ANDROID__ #define USE_SDL #include #endif #ifdef USE_SDL #include #endif #include #include #ifdef TRACY_ENABLE #include #else #define ZoneScoped #define ZoneScopedNC(NAME, COLOR) #endif #include "core/framebuffer.h" #include "core/log.h" #include "core/debugrenderer.h" #include "core/inputbuddy.h" #include "core/optionparser.h" #include "core/textrenderer.h" #include "core/texture.h" #include "core/util.h" #include "core/xrbuddy.h" #include "camerasconfig.h" #include "camerapathrenderer.h" #include "flycam.h" #include "gaussiancloud.h" #include "magiccarpet.h" #include "pointcloud.h" #include "pointrenderer.h" #include "splatrenderer.h" #include "vrconfig.h" enum optionIndex { UNKNOWN, OPENXR, FULLSCREEN, DEBUG, HELP, FP16, FP32, NOSH, }; const option::Descriptor usage[] = { { UNKNOWN, 0, "", "", option::Arg::None, "USAGE: splatapult [options] FILE.ply\n\nOptions:" }, { HELP, 0, "h", "help", option::Arg::None, " -h, --help Print usage and exit." }, { OPENXR, 0, "v", "openxr", option::Arg::None, " -v, --openxr Launch app in vr mode, using openxr runtime." }, { FULLSCREEN, 0, "f", "fullscren", option::Arg::None, " -f, --fullscreen Launch window in fullscreen." }, { DEBUG, 0, "d", "debug", option::Arg::None, " -d, --debug Enable verbose debug logging." }, { FP16, 0, "", "fp16", option::Arg::None, " --fp16 Use 16-bit half-precision floating frame buffer, to reduce color banding artifacts" }, { FP32, 0, "", "fp32", option::Arg::None, " --fp32 Use 32-bit floating point frame buffer, to reduce color banding even more" }, { NOSH, 0, "", "nosh", option::Arg::None, " --nosh Don't load/render full sh, this will reduce memory usage and higher performance" }, { UNKNOWN, 0, "", "", option::Arg::None, "\nExamples:\n splataplut data/test.ply\n splatapult -v data/test.ply" }, { 0, 0, 0, 0, 0, 0} }; const float Z_NEAR = 0.1f; const float Z_FAR = 1000.0f; const float FOVY = glm::radians(45.0f); const float MOVE_SPEED = 2.5f; const float ROT_SPEED = 1.15f; const glm::vec4 WHITE = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); const glm::vec4 BLACK = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); const int TEXT_NUM_ROWS = 25; #include #include #include // searches for file named configFilename, dir that contains plyFilename, it's parent and grandparent dirs. static std::string FindConfigFile(const std::string& plyFilename, const std::string& configFilename) { std::filesystem::path plyPath(plyFilename); if (!std::filesystem::exists(plyPath) || !std::filesystem::is_regular_file(plyPath)) { Log::E("PLY file does not exist or is not a file: \"%s\"", plyFilename.c_str()); return ""; } std::filesystem::path directory = plyPath.parent_path(); for (int i = 0; i < 3; ++i) // Check current, parent, and grandparent directories { std::filesystem::path configPath = directory / configFilename; if (std::filesystem::exists(configPath) && std::filesystem::is_regular_file(configPath)) { return configPath.string(); } if (directory.has_parent_path()) { directory = directory.parent_path(); } else { break; } } return ""; } static std::string GetFilenameWithoutExtension(const std::string& filepath) { std::filesystem::path pathObj(filepath); // Check if the path has a stem (the part of the path before the extension) if (pathObj.has_stem()) { return pathObj.stem().string(); } // If there is no stem, return an empty string return ""; } static std::string MakeVrConfigFilename(const std::string& plyFilename) { std::filesystem::path plyPath(plyFilename); std::filesystem::path directory = plyPath.parent_path(); std::string plyNoExt = GetFilenameWithoutExtension(plyFilename) + "_vr.json"; std::filesystem::path configPath = directory / plyNoExt; return configPath.string(); } static void Clear(glm::ivec2 windowSize, bool setViewport = true) { int width = windowSize.x; int height = windowSize.y; if (setViewport) { glViewport(0, 0, width, height); } // pre-multiplied alpha blending glEnable(GL_BLEND); glBlendEquation(GL_FUNC_ADD); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glm::vec4 clearColor(0.0f, 0.0f, 0.0f, 1.0f); glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // NOTE: if depth buffer has less then 24 bits, it can mess up splat rendering. glEnable(GL_DEPTH_TEST); } // Draw a textured quad over the entire screen. static void RenderDesktop(glm::ivec2 windowSize, std::shared_ptr desktopProgram, uint32_t colorTexture, bool adjustAspect) { int width = windowSize.x; int height = windowSize.y; glViewport(0, 0, width, height); glm::vec4 clearColor(0.0f, 0.0f, 0.0f, 1.0f); glClearColor(clearColor.x, clearColor.y, clearColor.z, clearColor.w); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glm::mat4 projMat = glm::ortho(0.0f, (float)width, 0.0f, (float)height, -10.0f, 10.0f); if (colorTexture > 0) { desktopProgram->Bind(); desktopProgram->SetUniform("modelViewProjMat", projMat); desktopProgram->SetUniform("color", glm::vec4(1.0f)); // use texture unit 0 for colorTexture glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, colorTexture); desktopProgram->SetUniform("colorTexture", 0); glm::vec2 xyLowerLeft(0.0f, 0.0f); glm::vec2 xyUpperRight((float)width, (float)height); if (adjustAspect) { xyLowerLeft = glm::vec2(0.0f, (height - width) / 2.0f); xyUpperRight = glm::vec2((float)width, (height + width) / 2.0f); } glm::vec2 uvLowerLeft(0.0f, 0.0f); glm::vec2 uvUpperRight(1.0f, 1.0f); float depth = -9.0f; glm::vec3 positions[] = {glm::vec3(xyLowerLeft, depth), glm::vec3(xyUpperRight.x, xyLowerLeft.y, depth), glm::vec3(xyUpperRight, depth), glm::vec3(xyLowerLeft.x, xyUpperRight.y, depth)}; desktopProgram->SetAttrib("position", positions); glm::vec2 uvs[] = {uvLowerLeft, glm::vec2(uvUpperRight.x, uvLowerLeft.y), uvUpperRight, glm::vec2(uvLowerLeft.x, uvUpperRight.y)}; desktopProgram->SetAttrib("uv", uvs); const size_t NUM_INDICES = 6; uint16_t indices[NUM_INDICES] = {0, 1, 2, 0, 2, 3}; glDrawElements(GL_TRIANGLES, NUM_INDICES, GL_UNSIGNED_SHORT, indices); } } static std::shared_ptr LoadPointCloud(const std::string& plyFilename, bool useLinearColors) { auto pointCloud = std::make_shared(useLinearColors); if (!pointCloud->ImportPly(plyFilename)) { Log::E("Error loading PointCloud!\n"); return nullptr; } return pointCloud; } static std::shared_ptr LoadGaussianCloud(const std::string& plyFilename, const App::Options& opt) { GaussianCloud::Options options = {0}; #ifdef __ANDROID__ options.importFullSH = false; options.exportFullSH = false; #else options.importFullSH = opt.importFullSH; options.exportFullSH = true; #endif auto gaussianCloud = std::make_shared(options); if (!gaussianCloud->ImportPly(plyFilename)) { Log::E("Error loading GaussianCloud!\n"); return nullptr; } return gaussianCloud; } static void PrintControls() { fprintf(stdout, "\ \n\ Desktop Controls\n\ --------------------\n\ * wasd - move\n\ * arrow keys - look\n\ * right mouse button - hold down for mouse look.\n\ * gamepad - if present, right stick to rotate, left stick to move, bumpers to roll\n\ * c - toggle between initial SfM point cloud (if present) and gaussian splats.\n\ * n - jump to next camera\n\ * p - jump to previous camera\n\ \n\ VR Controls\n\ ---------------\n\ * c - toggle between initial SfM point cloud (if present) and gaussian splats.\n\ * left stick - move\n\ * right stick - snap turn\n\ * f - show hide floor carpet.\n\ * single grab - translate the world.\n\ * double grab - rotate and translate the world.\n\ * triple grab - (double grab while trigger is depressed) scale, rotate and translate the world.\n\ * return - save the current position and orientation/scale of the world into a vr.json file.\n\ \n"); } App::App(MainContext& mainContextIn): mainContext(mainContextIn) { cameraIndex = 0; virtualLeftStick = glm::vec2(0.0f, 0.0f); virtualRightStick = glm::vec2(0.0f, 0.0f); mouseLookStick = glm::vec2(0.0f, 0.0f); mouseLook = false; virtualRoll = 0.0f; virtualUp = 0.0f; frameNum = 0; } App::ParseResult App::ParseArguments(int argc, const char* argv[]) { // skip program name if (argc > 0) { argc--; argv++; } option::Stats stats(usage, argc, argv); std::vector options(stats.options_max); std::vector buffer(stats.buffer_max); option::Parser parse(usage, argc, argv, options.data(), buffer.data()); if (parse.error()) { return ERROR_RESULT; } if (options[HELP] || argc == 0) { option::printUsage(std::cout, usage); PrintControls(); return QUIT_RESULT; } if (options[OPENXR]) { opt.vrMode = true; } if (options[FULLSCREEN]) { opt.fullscreen = true; } if (options[DEBUG]) { opt.debugLogging = true; } if (options[FP32]) { opt.frameBuffer = Options::FrameBuffer::Float; } else if (options[FP16]) { opt.frameBuffer = Options::FrameBuffer::HalfFloat; } opt.importFullSH = options[NOSH] ? false : true; bool unknownOptionFound = false; for (option::Option* opt = options[UNKNOWN]; opt; opt = opt->next()) { unknownOptionFound = true; std::cout << "Unknown option: " << std::string(opt->name,opt->namelen) << "\n"; } if (unknownOptionFound) { return ERROR_RESULT; } if (parse.nonOptionsCount() == 0) { std::cout << "Expected filename argument\n"; return ERROR_RESULT; } else { plyFilename = parse.nonOption(0); } Log::SetLevel(opt.debugLogging ? Log::Debug : Log::Warning); std::filesystem::path plyPath(plyFilename); if (!std::filesystem::exists(plyPath) || !std::filesystem::is_regular_file(plyPath)) { Log::E("Invalid file \"%s\"\n", plyFilename.c_str()); return ERROR_RESULT; } return SUCCESS_RESULT; } bool App::Init() { bool isFramebufferSRGBEnabled = opt.vrMode; #ifndef __ANDROID__ // AJT: ANDROID: TODO: make sure colors are accurate on android. if (isFramebufferSRGBEnabled) { // necessary for proper color conversion glEnable(GL_FRAMEBUFFER_SRGB); } else { glDisable(GL_FRAMEBUFFER_SRGB); } GLenum err = glewInit(); if (GLEW_OK != err) { Log::E("Error: %s\n", glewGetErrorString(err)); return false; } #endif debugRenderer = std::make_shared(); if (!debugRenderer->Init()) { Log::E("DebugRenderer Init failed\n"); return false; } textRenderer = std::make_shared(); if (!textRenderer->Init("font/JetBrainsMono-Medium.json", "font/JetBrainsMono-Medium.png")) { Log::E("TextRenderer Init failed\n"); return false; } if (opt.vrMode) { xrBuddy = std::make_shared(mainContext, glm::vec2(Z_NEAR, Z_FAR)); if (!xrBuddy->Init()) { Log::E("OpenXR Init failed\n"); return false; } } std::string camerasConfigFilename = FindConfigFile(plyFilename, "cameras.json"); if (!camerasConfigFilename.empty()) { camerasConfig = std::make_shared(); if (!camerasConfig->ImportJson(camerasConfigFilename)) { Log::W("Error loading cameras.json\n"); camerasConfig.reset(); } } else { Log::D("Could not find cameras.json\n"); } if (camerasConfig) { cameraPathRenderer = std::make_shared(); if (!cameraPathRenderer->Init(camerasConfig->GetCameraVec())) { Log::E("CameraPathRenderer Init failed\n"); return false; } } // search for vr config file // for example: if plyFilename is "input.ply", then search for "input_vr.json" std::string vrConfigBaseFilename = GetFilenameWithoutExtension(plyFilename) + "_vr.json"; std::string vrConfigFilename = FindConfigFile(plyFilename, vrConfigBaseFilename); if (!vrConfigFilename.empty()) { vrConfig = std::make_shared(); if (!vrConfig->ImportJson(vrConfigFilename)) { Log::I("Could not load vr.json\n"); vrConfig.reset(); } } else { Log::D("Could not find %s\n", vrConfigFilename.c_str()); // Where we'd like the vr config file to exist. vrConfigFilename = MakeVrConfigFilename(plyFilename); } glm::mat4 flyCamMat(1.0f); glm::mat4 floorMat(1.0f); if (camerasConfig) { flyCamMat = camerasConfig->GetCameraVec()[cameraIndex].mat; // initialize magicCarpet from first camera and estimated floor position. if (camerasConfig->GetNumCameras() > 0) { glm::vec3 floorNormal, floorPos; camerasConfig->EstimateFloorPlane(floorNormal, floorPos); glm::vec3 floorZ = camerasConfig->GetCameraVec()[0].mat[2]; glm::vec3 floorY = floorNormal; glm::vec3 floorX = glm::cross(floorY, floorZ); floorZ = glm::cross(floorX, floorY); floorMat = glm::mat4(glm::vec4(floorX, 0.0f), glm::vec4(floorY, 0.0f), glm::vec4(floorZ, 0.0f), glm::vec4(floorPos, 1.0f)); } } if (vrConfig) { floorMat = vrConfig->GetFloorMat(); if (!camerasConfig) { glm::vec3 pos = floorMat[3]; pos += glm::mat3(floorMat) * glm::vec3(0.0f, 1.5f, 0.0f); glm::mat4 adjustedFloorMat = floorMat; adjustedFloorMat[3] = glm::vec4(pos, 1.0f); flyCamMat = adjustedFloorMat; } } glm::vec3 flyCamPos, flyCamScale, floorMatUp; glm::quat flyCamRot; floorMatUp = glm::vec3(floorMat[1]); Decompose(flyCamMat, &flyCamScale, &flyCamRot, &flyCamPos); flyCam = std::make_shared(floorMatUp, flyCamPos, flyCamRot, MOVE_SPEED, ROT_SPEED); magicCarpet = std::make_shared(floorMat, MOVE_SPEED); if (!magicCarpet->Init(isFramebufferSRGBEnabled)) { Log::E("Error initalizing MagicCarpet\n"); return false; } std::string pointCloudFilename = FindConfigFile(plyFilename, "input.ply"); if (!pointCloudFilename.empty()) { pointCloud = LoadPointCloud(pointCloudFilename, isFramebufferSRGBEnabled); if (!pointCloud) { Log::E("Error loading PointCloud\n"); return false; } pointRenderer = std::make_shared(); if (!pointRenderer->Init(pointCloud, isFramebufferSRGBEnabled)) { Log::E("Error initializing point renderer!\n"); return false; } } else { Log::D("Could not find input.ply\n"); } gaussianCloud = LoadGaussianCloud(plyFilename, opt); if (!gaussianCloud) { Log::E("Error loading GaussianCloud\n"); return false; } #if 0 const uint32_t SPLAT_COUNT = 25000; glm::vec3 focalPoint = flyCam->GetCameraMat()[3]; //gaussianCloud->PruneSplats(glm::vec3(flyCam->GetCameraMat()[3]), SPLAT_COUNT); gaussianCloud->PruneSplats(focalPoint, SPLAT_COUNT); #endif splatRenderer = std::make_shared(); #if __ANDROID__ bool useRgcSortOverride = true; #else bool useRgcSortOverride = false; #endif if (!splatRenderer->Init(gaussianCloud, isFramebufferSRGBEnabled, useRgcSortOverride)) { Log::E("Error initializing splat renderer!\n"); return false; } if (opt.vrMode) { desktopProgram = std::make_shared(); std::string defines = "#define USE_SUPERSAMPLING\n"; desktopProgram->AddMacro("DEFINES", defines); if (!desktopProgram->LoadVertFrag("shader/desktop_vert.glsl", "shader/desktop_frag.glsl")) { Log::E("Error loading desktop shader!\n"); return 1; } xrBuddy->SetRenderCallback([this]( const glm::mat4& projMat, const glm::mat4& eyeMat, const glm::vec4& viewport, const glm::vec2& nearFar, int viewNum) { Clear(glm::ivec2(0, 0), false); glm::mat4 fullEyeMat = magicCarpet->GetCarpetMat() * eyeMat; if (opt.drawDebug) { debugRenderer->Render(fullEyeMat, projMat, viewport, nearFar); } if (cameraPathRenderer) { cameraPathRenderer->SetShowCameras(opt.drawCameraFrustums); cameraPathRenderer->SetShowPath(opt.drawCameraPath); cameraPathRenderer->Render(fullEyeMat, projMat, viewport, nearFar); } if (opt.drawCarpet) { magicCarpet->Render(fullEyeMat, projMat, viewport, nearFar); } if (opt.drawPointCloud && pointRenderer) { pointRenderer->Render(fullEyeMat, projMat, viewport, nearFar); } else { if (viewNum == 0) { splatRenderer->Sort(fullEyeMat, projMat, viewport, nearFar); } splatRenderer->Render(fullEyeMat, projMat, viewport, nearFar); } }); } if (!opt.vrMode && opt.frameBuffer != Options::FrameBuffer::Default) { desktopProgram = std::make_shared(); if (!desktopProgram->LoadVertFrag("shader/desktop_vert.glsl", "shader/desktop_frag.glsl")) { Log::E("Error loading desktop shader!\n"); return 1; } } #ifdef USE_SDL inputBuddy = std::make_shared(); inputBuddy->OnQuit([this]() { // forward this back to main quitCallback(); }); inputBuddy->OnResize([this](int newWidth, int newHeight) { glViewport(0, 0, newWidth, newHeight); resizeCallback(newWidth, newHeight); }); inputBuddy->OnKey(SDLK_ESCAPE, [this](bool down, uint16_t mod) { quitCallback(); }); inputBuddy->OnKey(SDLK_c, [this](bool down, uint16_t mod) { if (down) { opt.drawPointCloud = !opt.drawPointCloud; } }); inputBuddy->OnKey(SDLK_n, [this](bool down, uint16_t mod) { if (down && camerasConfig) { cameraIndex++; if (cameraIndex >= (int)camerasConfig->GetNumCameras()) { cameraIndex -= (int)camerasConfig->GetNumCameras(); } flyCam->SetCameraMat(camerasConfig->GetCameraVec()[cameraIndex].mat); } }); inputBuddy->OnKey(SDLK_p, [this](bool down, uint16_t mod) { if (down && camerasConfig) { cameraIndex--; if (cameraIndex < 0) { cameraIndex += (int)camerasConfig->GetNumCameras(); } flyCam->SetCameraMat(camerasConfig->GetCameraVec()[cameraIndex].mat); } }); inputBuddy->OnKey(SDLK_f, [this](bool down, uint16_t mod) { if (down) { opt.drawCarpet = !opt.drawCarpet; } }); inputBuddy->OnKey(SDLK_y, [this](bool down, uint16_t mod) { if (down) { opt.drawCameraFrustums = !opt.drawCameraFrustums; } }); inputBuddy->OnKey(SDLK_h, [this](bool down, uint16_t mod) { if (down) { opt.drawCameraPath = !opt.drawCameraPath; } }); inputBuddy->OnKey(SDLK_RETURN, [this, vrConfigFilename](bool down, uint16_t mod) { if (down) { if (!vrConfig) { vrConfig = std::make_shared(); } if (opt.vrMode) { vrConfig->SetFloorMat(magicCarpet->GetCarpetMat()); } else { glm::mat4 headMat = flyCam->GetCameraMat(); glm::vec3 pos = headMat[3]; pos -= glm::mat3(headMat) * glm::vec3(0.0f, 1.5f, 0.0f); glm::mat4 floorMat = headMat; floorMat[3] = glm::vec4(pos, 1.0f); vrConfig->SetFloorMat(floorMat); } if (vrConfig->ExportJson(vrConfigFilename)) { Log::I("Wrote \"%s\"\n", vrConfigFilename.c_str()); } else { Log::E("Writing \"%s\" failed\n", vrConfigFilename.c_str()); } } }); inputBuddy->OnKey(SDLK_F1, [this](bool down, uint16_t mod) { if (down) { opt.drawFps = !opt.drawFps; } }); inputBuddy->OnKey(SDLK_a, [this](bool down, uint16_t mod) { virtualLeftStick.x += down ? -1.0f : 1.0f; }); inputBuddy->OnKey(SDLK_d, [this](bool down, uint16_t mod) { virtualLeftStick.x += down ? 1.0f : -1.0f; }); inputBuddy->OnKey(SDLK_w, [this](bool down, uint16_t mod) { virtualLeftStick.y += down ? 1.0f : -1.0f; }); inputBuddy->OnKey(SDLK_s, [this](bool down, uint16_t mod) { virtualLeftStick.y += down ? -1.0f : 1.0f; }); inputBuddy->OnKey(SDLK_LEFT, [this](bool down, uint16_t mod) { virtualRightStick.x += down ? -1.0f : 1.0f; }); inputBuddy->OnKey(SDLK_RIGHT, [this](bool down, uint16_t mod) { virtualRightStick.x += down ? 1.0f : -1.0f; }); inputBuddy->OnKey(SDLK_UP, [this](bool down, uint16_t mod) { virtualRightStick.y += down ? 1.0f : -1.0f; }); inputBuddy->OnKey(SDLK_DOWN, [this](bool down, uint16_t mod) { virtualRightStick.y += down ? -1.0f : 1.0f; }); inputBuddy->OnKey(SDLK_q, [this](bool down, uint16_t mod) { virtualRoll += down ? -1.0f : 1.0f; }); inputBuddy->OnKey(SDLK_e, [this](bool down, uint16_t mod) { virtualRoll += down ? 1.0f : -1.0f; }); inputBuddy->OnKey(SDLK_t, [this](bool down, uint16_t mod) { virtualUp += down ? 1.0f : -1.0f; }); inputBuddy->OnKey(SDLK_g, [this](bool down, uint16_t mod) { virtualUp += down ? -1.0f : 1.0f; }); inputBuddy->OnMouseButton([this](uint8_t button, bool down, glm::ivec2 pos) { if (button == 3) // right button { if (mouseLook != down) { inputBuddy->SetRelativeMouseMode(down); } mouseLook = down; } }); inputBuddy->OnMouseMotion([this](glm::ivec2 pos, glm::ivec2 rel) { if (mouseLook) { const float MOUSE_SENSITIVITY = 0.001f; mouseLookStick.x += rel.x * MOUSE_SENSITIVITY; mouseLookStick.y -= rel.y * MOUSE_SENSITIVITY; } }); #endif // USE_SDL fpsText = textRenderer->AddScreenTextWithDropShadow(glm::ivec2(0, 0), (int)TEXT_NUM_ROWS, WHITE, BLACK, "fps:"); return true; } void App::ProcessEvent(const SDL_Event& event) { #ifdef USE_SDL inputBuddy->ProcessEvent(event); #endif } void App::UpdateFps(float fps) { std::string text = "fps: " + std::to_string((int)fps); textRenderer->RemoveText(fpsText); fpsText = textRenderer->AddScreenTextWithDropShadow(glm::ivec2(0, 0), TEXT_NUM_ROWS, WHITE, BLACK, text); //#define FIND_BEST_NUM_BLOCKS_PER_WORKGROUP #ifdef FIND_BEST_NUM_BLOCKS_PER_WORKGROUP Log::E("%s\n", text.c_str()); fpsVec.push_back(fps); const uint32_t STEP_SIZE = 64; if (fpsVec.size() == 2) { float dFps = fpsVec[1] - fpsVec[0]; uint32_t x = splatRenderer->numBlocksPerWorkgroup - STEP_SIZE; Log::E(" (%.3f -> %.3f) dFps = %.3f, x = %u\n", fpsVec[0], fpsVec[1], dFps, x); if (dFps < 0) { Log::E(" FPS DOWN, new x = %u\n", x - STEP_SIZE); if (splatRenderer->numBlocksPerWorkgroup > STEP_SIZE) { splatRenderer->numBlocksPerWorkgroup = x - STEP_SIZE; } } else { Log::E(" FPS UP, new x = %u\n", x + STEP_SIZE); splatRenderer->numBlocksPerWorkgroup = x + STEP_SIZE; } fpsVec.clear(); } else { splatRenderer->numBlocksPerWorkgroup = splatRenderer->numBlocksPerWorkgroup + STEP_SIZE; } #endif } bool App::Process(float dt) { if (opt.vrMode) { if (!xrBuddy->PollEvents()) { Log::E("xrBuddy PollEvents failed\n"); return false; } if (!xrBuddy->SyncInput()) { Log::E("xrBuddy SyncInput failed\n"); return false; } // copy vr input into MagicCarpet MagicCarpet::Pose headPose, rightPose, leftPose; if (!xrBuddy->GetActionPosition("head_pose", &headPose.pos, &headPose.posValid, &headPose.posTracked)) { Log::W("xrBuddy GetActionPosition(head_pose) failed\n"); } if (!xrBuddy->GetActionOrientation("head_pose", &headPose.rot, &headPose.rotValid, &headPose.rotTracked)) { Log::W("xrBuddy GetActionOrientation(head_pose) failed\n"); } xrBuddy->GetActionPosition("l_aim_pose", &leftPose.pos, &leftPose.posValid, &leftPose.posTracked); xrBuddy->GetActionOrientation("l_aim_pose", &leftPose.rot, &leftPose.rotValid, &leftPose.rotTracked); xrBuddy->GetActionPosition("r_aim_pose", &rightPose.pos, &rightPose.posValid, &rightPose.posTracked); xrBuddy->GetActionOrientation("r_aim_pose", &rightPose.rot, &rightPose.rotValid, &rightPose.rotTracked); glm::vec2 leftStick(0.0f, 0.0f); glm::vec2 rightStick(0.0f, 0.0f); bool valid = false; bool changed = false; xrBuddy->GetActionVec2("l_stick", &leftStick, &valid, &changed); xrBuddy->GetActionVec2("r_stick", &rightStick, &valid, &changed); // Convert trackpad into a "stick", for HTC Vive controllers glm::vec2 leftTrackpadStick(0.0f, 0.0f); bool leftTrackpadClick = false; xrBuddy->GetActionBool("l_trackpad_click", &leftTrackpadClick, &valid, &changed); if (leftTrackpadClick && valid) { xrBuddy->GetActionFloat("l_trackpad_x", &leftTrackpadStick.x, &valid, &changed); xrBuddy->GetActionFloat("l_trackpad_y", &leftTrackpadStick.y, &valid, &changed); } else { leftTrackpadStick = glm::vec2(0.0f, 0.0f); } glm::vec2 rightTrackpadStick(0.0f, 0.0f); bool rightTrackpadClick = false; xrBuddy->GetActionBool("r_trackpad_click", &rightTrackpadClick, &valid, &changed); if (rightTrackpadClick && valid) { xrBuddy->GetActionFloat("r_trackpad_x", &rightTrackpadStick.x, &valid, &changed); xrBuddy->GetActionFloat("r_trackpad_y", &rightTrackpadStick.y, &valid, &changed); } else { rightTrackpadStick = glm::vec2(0.0f, 0.0f); } MagicCarpet::ButtonState buttonState; xrBuddy->GetActionBool("l_select_click", &buttonState.leftTrigger, &valid, &changed); xrBuddy->GetActionBool("r_select_click", &buttonState.rightTrigger, &valid, &changed); xrBuddy->GetActionBool("l_squeeze_click", &buttonState.leftGrip, &valid, &changed); xrBuddy->GetActionBool("r_squeeze_click", &buttonState.rightGrip, &valid, &changed); magicCarpet->Process(headPose, leftPose, rightPose, leftStick + leftTrackpadStick, rightStick + rightTrackpadStick, buttonState, dt); } #ifdef USE_SDL InputBuddy::Joypad joypad = inputBuddy->GetJoypad(); float roll = 0.0f; roll -= joypad.lb ? 1.0f : 0.0f; roll += joypad.rb ? 1.0f : 0.0f; flyCam->Process(glm::clamp(joypad.leftStick + virtualLeftStick, -1.0f, 1.0f), glm::clamp(joypad.rightStick + virtualRightStick, -1.0f, 1.0f) + mouseLookStick / (dt > 0.0f ? dt : 1.0f), glm::clamp(roll + virtualRoll, -1.0f, 1.0f), glm::clamp(virtualUp, -1.0f, 1.0f), dt); mouseLookStick = glm::vec2(0.0f, 0.0f); #endif return true; } bool App::Render(float dt, const glm::ivec2& windowSize) { int width = windowSize.x; int height = windowSize.y; if (opt.vrMode) { if (xrBuddy->SessionReady()) { if (!xrBuddy->RenderFrame()) { Log::E("xrBuddy RenderFrame failed\n"); return false; } } else { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } #ifndef __ANDROID__ // render desktop. Clear(windowSize, true); RenderDesktop(windowSize, desktopProgram, xrBuddy->GetColorTexture(), true); if (opt.drawFps) { glm::vec4 viewport(0.0f, 0.0f, (float)width, (float)height); glm::vec2 nearFar(Z_NEAR, Z_FAR); glm::mat4 projMat = glm::perspective(FOVY, (float)width / (float)height, Z_NEAR, Z_FAR); textRenderer->Render(glm::mat4(1.0f), projMat, viewport, nearFar); } #endif } else { // lazy init of fbo, fbo is only used for HalfFloat, Float option. if (opt.frameBuffer != Options::FrameBuffer::Default && fboSize != windowSize) { fbo = std::make_shared(); Texture::Params texParams; texParams.minFilter = FilterType::Nearest; texParams.magFilter = FilterType::Nearest; texParams.sWrap = WrapType::ClampToEdge; texParams.tWrap = WrapType::ClampToEdge; if (opt.frameBuffer == Options::FrameBuffer::HalfFloat) { fboColorTex = std::make_shared(windowSize.x, windowSize.y, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, texParams); } else if (opt.frameBuffer == Options::FrameBuffer::Float) { fboColorTex = std::make_shared(windowSize.x, windowSize.y, GL_RGBA32F, GL_RGBA, GL_FLOAT, texParams); } else { Log::E("BAD opt.frameBuffer type!\n"); } fbo->AttachColor(fboColorTex); fboSize = windowSize; } if (opt.frameBuffer != Options::FrameBuffer::Default && fbo) { fbo->Bind(); } Clear(windowSize, true); glm::mat4 cameraMat = flyCam->GetCameraMat(); glm::vec4 viewport(0.0f, 0.0f, (float)width, (float)height); glm::vec2 nearFar(Z_NEAR, Z_FAR); glm::mat4 projMat = glm::perspective(FOVY, (float)width / (float)height, Z_NEAR, Z_FAR); if (opt.drawDebug) { debugRenderer->Render(cameraMat, projMat, viewport, nearFar); } if (cameraPathRenderer) { cameraPathRenderer->SetShowCameras(opt.drawCameraFrustums); cameraPathRenderer->SetShowPath(opt.drawCameraPath); cameraPathRenderer->Render(cameraMat, projMat, viewport, nearFar); } if (opt.drawCarpet) { magicCarpet->Render(cameraMat, projMat, viewport, nearFar); } if (opt.drawPointCloud && pointRenderer) { pointRenderer->Render(cameraMat, projMat, viewport, nearFar); } else { splatRenderer->Sort(cameraMat, projMat, viewport, nearFar); splatRenderer->Render(cameraMat, projMat, viewport, nearFar); } if (opt.drawFps) { textRenderer->Render(cameraMat, projMat, viewport, nearFar); } if (opt.frameBuffer != Options::FrameBuffer::Default && fbo) { // render fbo colorTexture as a full screen quad to the default fbo glBindFramebuffer(GL_FRAMEBUFFER, 0); Clear(windowSize, true); RenderDesktop(windowSize, desktopProgram, fbo->GetColorTexture()->texture, false); } } debugRenderer->EndFrame(); frameNum++; return true; } void App::OnQuit(const VoidCallback& cb) { quitCallback = cb; } void App::OnResize(const ResizeCallback& cb) { resizeCallback = cb; } ================================================ FILE: src/app.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include "maincontext.h" class CamerasConfig; class CameraPathRenderer; class DebugRenderer; class FlyCam; struct FrameBuffer; class GaussianCloud; class InputBuddy; class MagicCarpet; class PointCloud; class PointRenderer; class Program; class SplatRenderer; class TextRenderer; struct Texture; class VrConfig; class XrBuddy; union SDL_Event; class App { public: App(MainContext& mainContextIn); enum ParseResult { SUCCESS_RESULT, ERROR_RESULT, QUIT_RESULT }; ParseResult ParseArguments(int argc, const char* argv[]); bool Init(); bool IsFullscreen() const { return opt.fullscreen; } void UpdateFps(float fps); void ProcessEvent(const SDL_Event& event); bool Process(float dt); bool Render(float dt, const glm::ivec2& windowSize); using VoidCallback = std::function; void OnQuit(const VoidCallback& cb); using ResizeCallback = std::function; void OnResize(const ResizeCallback& cb); struct Options { enum class FrameBuffer { Default, HalfFloat, Float }; bool vrMode = false; bool fullscreen = false; FrameBuffer frameBuffer = FrameBuffer::Default; bool drawCarpet = false; bool drawPointCloud = false; bool drawDebug = true; bool debugLogging = false; bool drawFps = true; bool drawCameraFrustums = false; bool drawCameraPath = false; bool importFullSH = true; }; protected: MainContext& mainContext; Options opt; std::string plyFilename; std::shared_ptr debugRenderer; std::shared_ptr cameraPathRenderer; std::shared_ptr textRenderer; std::shared_ptr xrBuddy; std::shared_ptr camerasConfig; std::shared_ptr vrConfig; int cameraIndex; std::shared_ptr flyCam; std::shared_ptr magicCarpet; std::shared_ptr pointCloud; std::shared_ptr gaussianCloud; std::shared_ptr pointRenderer; std::shared_ptr splatRenderer; std::shared_ptr desktopProgram; std::shared_ptr fbo; glm::ivec2 fboSize = {0, 0}; std::shared_ptr fboColorTex; std::shared_ptr inputBuddy; glm::vec2 virtualLeftStick; glm::vec2 virtualRightStick; glm::vec2 mouseLookStick; bool mouseLook; float virtualRoll; float virtualUp; uint32_t fpsText; uint32_t frameNum; VoidCallback quitCallback; ResizeCallback resizeCallback; std::vector fpsVec; }; ================================================ FILE: src/camerapathrenderer.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "camerapathrenderer.h" #ifdef __ANDROID__ #include #include #include #include #else #include #endif #include "core/log.h" #include "core/util.h" #include "camerasconfig.h" CameraPathRenderer::CameraPathRenderer() { } CameraPathRenderer::~CameraPathRenderer() { } bool CameraPathRenderer::Init(const std::vector& cameraVec) { ddProg = std::make_shared(); if (!ddProg->LoadVertFrag("shader/debugdraw_vert.glsl", "shader/debugdraw_frag.glsl")) { Log::E("Error loading CameraPathRenderer shader!\n"); return false; } BuildCamerasVao(cameraVec); BuildPathVao(cameraVec); return true; } // viewport = (x, y, width, height) void CameraPathRenderer::Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar) { if (!showCameras && !showPath) { return; } GL_ERROR_CHECK("CameraPathRenderer::Render() begin"); glm::mat4 modelViewProjMat = projMat * glm::inverse(cameraMat); ddProg->Bind(); ddProg->SetUniform("modelViewProjMat", modelViewProjMat); if (showCameras) { camerasVao->Bind(); glDrawElements(GL_LINES, numCameraVerts, GL_UNSIGNED_INT, nullptr); camerasVao->Unbind(); } if (showPath) { pathVao->Bind(); glDrawElements(GL_LINES, numPathVerts, GL_UNSIGNED_INT, nullptr); pathVao->Unbind(); } GL_ERROR_CHECK("CameraPathRenderer::Render() draw"); } void CameraPathRenderer::BuildCamerasVao(const std::vector& cameraVec) { camerasVao = std::make_shared(); const uint32_t NUM_LINES = 8; const float FRUSTUM_LEN = 0.1f; const glm::vec4 FRUSTUM_COLOR(0.0f, 1.0f, 0.0f, 1.0f); numCameraVerts = (uint32_t)cameraVec.size() * NUM_LINES * 2; std::vector posVec; posVec.reserve(numCameraVerts); std::vector colVec; colVec.reserve(numCameraVerts); std::vector indexVec; indexVec.reserve(numCameraVerts); // build lines for each frustum for (auto& c : cameraVec) { float xRadius = FRUSTUM_LEN / cosf(c.fov.x / 2.0f); float xOffset = xRadius * sinf(c.fov.x / 2.0f); float yRadius = FRUSTUM_LEN / cosf(c.fov.y / 2.0f); float yOffset = yRadius * sinf(c.fov.y / 2.0f); const uint32_t NUM_FRUSTUM_VERTS = 5; glm::vec3 verts[NUM_FRUSTUM_VERTS] = { glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(xOffset, yOffset, -FRUSTUM_LEN), glm::vec3(-xOffset, yOffset, -FRUSTUM_LEN), glm::vec3(-xOffset, -yOffset, -FRUSTUM_LEN), glm::vec3(xOffset, -yOffset, -FRUSTUM_LEN) }; for (int i = 0; i < NUM_FRUSTUM_VERTS; i++) { verts[i] = XformPoint(c.mat, verts[i]); } posVec.push_back(verts[0]); posVec.push_back(verts[1]); posVec.push_back(verts[0]); posVec.push_back(verts[2]); posVec.push_back(verts[0]); posVec.push_back(verts[3]); posVec.push_back(verts[0]); posVec.push_back(verts[4]); posVec.push_back(verts[1]); posVec.push_back(verts[2]); posVec.push_back(verts[2]); posVec.push_back(verts[3]); posVec.push_back(verts[3]); posVec.push_back(verts[4]); posVec.push_back(verts[4]); posVec.push_back(verts[1]); for (int i = 0; i < NUM_LINES; i++) { colVec.push_back(FRUSTUM_COLOR); colVec.push_back(FRUSTUM_COLOR); } } auto posBuffer = std::make_shared(GL_ARRAY_BUFFER, posVec); auto colBuffer = std::make_shared(GL_ARRAY_BUFFER, colVec); // build element array assert(numCameraVerts <= std::numeric_limits::max()); for (uint32_t i = 0; i < numCameraVerts; i++) { indexVec.push_back(i); } auto indexBuffer = std::make_shared(GL_ELEMENT_ARRAY_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT); // setup vertex array object with buffers camerasVao->SetAttribBuffer(ddProg->GetAttribLoc("position"), posBuffer); camerasVao->SetAttribBuffer(ddProg->GetAttribLoc("color"), colBuffer); camerasVao->SetElementBuffer(indexBuffer); } void CameraPathRenderer::BuildPathVao(const std::vector& cameraVec) { pathVao = std::make_shared(); const uint32_t NUM_LINES = 1; const glm::vec4 PATH_COLOR(0.0f, 1.0f, 1.0f, 1.0f); numPathVerts = (uint32_t)cameraVec.size() * NUM_LINES * 2; std::vector posVec; posVec.reserve(numPathVerts); std::vector colVec; colVec.reserve(numPathVerts); std::vector indexVec; indexVec.reserve(numPathVerts); // build lines for each path segment if (cameraVec.size() > 1) { const Camera* prev = &cameraVec[0]; for (size_t i = 1; i < cameraVec.size(); i++) { const Camera* curr = cameraVec.data() + i; glm::vec3 prevPos = glm::vec3(prev->mat[3]); glm::vec3 currPos = glm::vec3(curr->mat[3]); posVec.push_back(prevPos); posVec.push_back(currPos); colVec.push_back(PATH_COLOR); colVec.push_back(PATH_COLOR); prev = curr; } } else { posVec.push_back(glm::vec3(0.0f, 0.0f, 0.0f)); posVec.push_back(glm::vec3(0.0f, 0.0f, 0.0f)); colVec.push_back(PATH_COLOR); colVec.push_back(PATH_COLOR); } auto posBuffer = std::make_shared(GL_ARRAY_BUFFER, posVec); auto colBuffer = std::make_shared(GL_ARRAY_BUFFER, colVec); // build element array assert(numPathVerts <= std::numeric_limits::max()); for (uint32_t i = 0; i < numPathVerts; i++) { indexVec.push_back(i); } auto indexBuffer = std::make_shared(GL_ELEMENT_ARRAY_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT); // setup vertex array object with buffers pathVao->SetAttribBuffer(ddProg->GetAttribLoc("position"), posBuffer); pathVao->SetAttribBuffer(ddProg->GetAttribLoc("color"), colBuffer); pathVao->SetElementBuffer(indexBuffer); } ================================================ FILE: src/camerapathrenderer.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include "core/program.h" #include "core/vertexbuffer.h" struct Camera; class CameraPathRenderer { public: CameraPathRenderer(); ~CameraPathRenderer(); bool Init(const std::vector& cameraVec); void SetShowCameras(bool showCamerasIn) { showCameras = showCamerasIn; } void SetShowPath(bool showPathIn) { showPath = showPathIn; } // viewport = (x, y, width, height) void Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar); protected: void BuildCamerasVao(const std::vector& cameraVec); void BuildPathVao(const std::vector& cameraVec); std::shared_ptr ddProg; std::shared_ptr camerasVao; uint32_t numCameraVerts; std::shared_ptr pathVao; uint32_t numPathVerts; bool showCameras = true; bool showPath = true; }; ================================================ FILE: src/camerasconfig.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "camerasconfig.h" #include #include #include #include "core/log.h" #include "core/util.h" CamerasConfig::CamerasConfig() { } bool CamerasConfig::ImportJson(const std::string& jsonFilename) { std::ifstream f(jsonFilename); if (f.fail()) { return false; } try { nlohmann::json data = nlohmann::json::parse(f); for (auto&& o : data) { int id = o["id"].template get(); nlohmann::json jPos = o["position"]; glm::vec3 pos(jPos[0].template get(), jPos[1].template get(), jPos[2].template get()); nlohmann::json jRot = o["rotation"]; glm::mat3 rot(jRot[0][0].template get(), jRot[1][0].template get(), jRot[2][0].template get(), jRot[0][1].template get(), jRot[1][1].template get(), jRot[2][1].template get(), jRot[0][2].template get(), jRot[1][2].template get(), jRot[2][2].template get()); int width = o["width"].template get(); int height = o["height"].template get(); float fx = o["fx"].template get(); float fy = o["fy"].template get(); glm::vec2 fov(2.0f * atanf(width / (2.0f * fx)), 2.0f * atanf(height / (2.0f * fx))); glm::mat4 mat(glm::vec4(rot[0], 0.0f), glm::vec4(-rot[1], 0.0f), glm::vec4(-rot[2], 0.0f), glm::vec4(pos, 1.0f)); // swizzle rot to make -z forward and y up. cameraVec.emplace_back(Camera{mat, fov}); } } catch (const nlohmann::json::exception& e) { std::string s = e.what(); Log::E("CamerasConfig::ImportJson exception: %s\n", s.c_str()); return false; } return true; } void CamerasConfig::EstimateFloorPlane(glm::vec3& normalOut, glm::vec3& posOut) const { if (cameraVec.empty()) { normalOut = glm::vec3(0.0f, 1.0f, 0.0f); posOut = glm::vec3(0.0f, 0.0f, 0.0f); } glm::vec3 avgUp(0.0f, 0.0f, 0.0f); float weight = 1.0f / (float)cameraVec.size(); for (auto&& c : cameraVec) { glm::vec3 up(c.mat[1]); avgUp += weight * up; } avgUp = SafeNormalize(avgUp, glm::vec3(0.0f, 1.0f, 0.0f)); float avgDist = 0.0f; for (auto&& c : cameraVec) { glm::vec3 pos(c.mat[3]); avgDist += weight * glm::dot(pos, avgUp); } normalOut = avgUp; posOut = avgUp * avgDist; } ================================================ FILE: src/camerasconfig.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include struct Camera { glm::mat4 mat; // inverse view matrix glm::vec2 fov; }; class CamerasConfig { public: CamerasConfig(); bool ImportJson(const std::string& jsonFilename); const std::vector& GetCameraVec() const { return cameraVec; } size_t GetNumCameras() const { return cameraVec.size(); } void EstimateFloorPlane(glm::vec3& normalOut, glm::vec3& posOut) const; protected: std::vector cameraVec; }; ================================================ FILE: src/core/binaryattribute.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "binaryattribute.h" static uint32_t propertyTypeSizeArr[(size_t)BinaryAttribute::Type::NumTypes] = { 0, // Unknown sizeof(int8_t), // Char sizeof(uint8_t), // UChar sizeof(int16_t), // Short sizeof(uint16_t), // UShort sizeof(int32_t), // Int sizeof(uint32_t), // UInt sizeof(float), // Float sizeof(double) // Double }; BinaryAttribute::BinaryAttribute(Type typeIn, size_t offsetIn) : type(typeIn), size(propertyTypeSizeArr[(uint32_t)typeIn]), offset(offsetIn) { ; } ================================================ FILE: src/core/binaryattribute.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include class BinaryAttribute { public: enum class Type { Unknown, Char, UChar, Short, UShort, Int, UInt, Float, Double, NumTypes }; BinaryAttribute() : type(Type::Unknown), size(0), offset(0) {} BinaryAttribute(Type typeIn, size_t offsetIn); template const T* Get(const void* data) const { if (type == Type::Unknown) { return nullptr; } else { assert(size == sizeof(T)); return reinterpret_cast(static_cast(data) + offset); } } template T* Get(void* data) { if (type == Type::Unknown) { return nullptr; } else { assert(size == sizeof(T)); return reinterpret_cast(static_cast(data) + offset); } } template const T Read(const void* data) const { const T* ptr = Get(data); return ptr ? *ptr : 0; } template bool Write(void* data, const T& val) { T* ptr = Get(data); if (ptr) { *ptr = val; return true; } else { return false; } } template void ForEachMut(void* data, size_t stride, size_t count, const std::function& cb) { assert(type != Type::Unknown); assert(data); uint8_t* ptr = (uint8_t*)data; for (size_t i = 0; i < count; i++) { cb((T*)(ptr + offset)); ptr += stride; } } template void ForEach(const void* data, size_t stride, size_t count, const std::function& cb) const { assert(type != Type::Unknown); assert(data); const uint8_t* ptr = (const uint8_t*)data; for (size_t i = 0; i < count; i++) { cb((const T*)(ptr + offset)); ptr += stride; } } Type type; size_t size; size_t offset; }; ================================================ FILE: src/core/debugrenderer.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "debugrenderer.h" #ifdef __ANDROID__ #include #include #include #include #else #include #define GL_GLEXT_PROTOTYPES 1 #include #include #endif #include #include #include "log.h" #include "util.h" #include "program.h" DebugRenderer::DebugRenderer() { } bool DebugRenderer::Init() { ddProg = std::make_shared(); if (!ddProg->LoadVertFrag("shader/debugdraw_vert.glsl", "shader/debugdraw_frag.glsl")) { Log::E("Error loading DebugRenderer shader!\n"); return false; } return true; } void DebugRenderer::Line(const glm::vec3& start, const glm::vec3& end, const glm::vec3& color) { linePositionVec.push_back(start); linePositionVec.push_back(end); lineColorVec.push_back(color); lineColorVec.push_back(color); } void DebugRenderer::Transform(const glm::mat4& m, float axisLen) { glm::vec3 x = glm::vec3(m[0]); glm::vec3 y = glm::vec3(m[1]); glm::vec3 z = glm::vec3(m[2]); x = axisLen * glm::normalize(x); y = axisLen * glm::normalize(y); z = axisLen * glm::normalize(z); glm::vec3 p = glm::vec3(m[3]); Line(p, p + x, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f)); Line(p, p + y, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)); Line(p, p + z, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f)); } void DebugRenderer::Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar) { ddProg->Bind(); glm::mat4 modelViewProjMat = projMat * glm::inverse(cameraMat); ddProg->SetUniform("modelViewProjMat", modelViewProjMat); ddProg->SetAttrib("position", linePositionVec.data()); ddProg->SetAttrib("color", lineColorVec.data()); glDrawArrays(GL_LINES, 0, (GLsizei)linePositionVec.size()); } void DebugRenderer::EndFrame() { linePositionVec.clear(); lineColorVec.clear(); } ================================================ FILE: src/core/debugrenderer.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include class Program; class DebugRenderer { public: DebugRenderer(); bool Init(); // viewport = (x, y, width, height) void Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar); // call at end of frame. void EndFrame(); void Line(const glm::vec3& start, const glm::vec3& end, const glm::vec3& color); void Transform(const glm::mat4& m, float axisLen = 1.0f); protected: std::shared_ptr ddProg; std::vector linePositionVec; std::vector lineColorVec; }; ================================================ FILE: src/core/framebuffer.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "framebuffer.h" #ifdef __ANDROID__ #include #include #include #include #else #include #define GL_GLEXT_PROTOTYPES 1 #include #include #endif #include "texture.h" FrameBuffer::FrameBuffer() { glGenFramebuffers(1, &fbo); } FrameBuffer::~FrameBuffer() { glDeleteFramebuffers(1, &fbo); fbo = 0; } void FrameBuffer::Bind() const { glBindFramebuffer(GL_FRAMEBUFFER, fbo); } void FrameBuffer::AttachColor(std::shared_ptr colorTex) { Bind(); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex->texture, 0); colorAttachment = colorTex; } void FrameBuffer::AttachDepth(std::shared_ptr depthTex) { Bind(); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTex->texture, 0); depthAttachment = depthTex; } void FrameBuffer::AttachStencil(std::shared_ptr stencilTex) { Bind(); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, stencilTex->texture, 0); stencilAttachment = stencilTex; } bool FrameBuffer::IsComplete() const { return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; } ================================================ FILE: src/core/framebuffer.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include struct Texture; struct FrameBuffer { FrameBuffer(); ~FrameBuffer(); void Bind() const; void AttachColor(std::shared_ptr colorTex); void AttachDepth(std::shared_ptr depthTex); void AttachStencil(std::shared_ptr stencilTex); bool IsComplete() const; std::shared_ptr GetColorTexture() const { return colorAttachment; } std::shared_ptr GetDepthTexture() const { return depthAttachment; } std::shared_ptr GetStencilTexture() const { return stencilAttachment; } uint32_t fbo; std::shared_ptr colorAttachment; std::shared_ptr depthAttachment; std::shared_ptr stencilAttachment; }; ================================================ FILE: src/core/image.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "image.h" //#include "util.h" #include "log.h" #include extern "C" { #include } #include "util.h" Image::Image() : width(0), height(0), pixelFormat(PixelFormat::R), isSRGB(false) { } bool Image::Load(const std::string& filenameIn) { std::string fullFilename = GetRootPath() + filenameIn; const char* filename = fullFilename.c_str(); #ifdef _WIN32 FILE *fp = NULL; fopen_s(&fp, filename, "rb"); #else FILE *fp = fopen(filename, "rb"); #endif if (!fp) { Log::E("Failed to load texture \"%s\"\n", filename); return false; } unsigned char header[8]; fread(header, 1, 8, fp); if (png_sig_cmp(header, 0, 8)) { Log::E("Texture \"%s\" is not a valid PNG file\n", filename); return false; } png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!png_ptr) { Log::E("png_create_read_struct() failed\n"); return false; } png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { Log::E("png_create_info_struct() failed\n"); png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL); return false; } png_init_io(png_ptr, fp); png_set_sig_bytes(png_ptr, 8); png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL); png_byte** row_pointers = png_get_rows(png_ptr, info_ptr); png_uint_32 w, h; int bit_depth, color_type; png_get_IHDR(png_ptr, info_ptr, &w, &h, &bit_depth, &color_type, NULL, NULL, NULL); bool loaded = false; if (bit_depth != 8) { Log::E("bad bit depth for texture \"%s\n", filename); } else { int pixelSize; switch (color_type) { case PNG_COLOR_TYPE_GRAY: pixelFormat = PixelFormat::R; pixelSize = 1; break; case PNG_COLOR_TYPE_GA: pixelFormat = PixelFormat::RA; pixelSize = 2; break; case PNG_COLOR_TYPE_RGB: pixelFormat = PixelFormat::RGB; pixelSize = 3; break; case PNG_COLOR_TYPE_RGBA: pixelFormat = PixelFormat::RGBA; pixelSize = 4; break; default: Log::E("unsupported pixel format %d for image \"%s\n", color_type, filename); return false; } width = w; height = h; data.resize(width * height * pixelSize); // copy row pointers into data vector for (int i = 0; i < (int)height; ++i) { memcpy(&data[0] + i * width * pixelSize, row_pointers[height - 1 - i], width * pixelSize); } // pre-multiply alpha MultiplyAlpha(); loaded = true; } png_destroy_info_struct(png_ptr, &info_ptr); png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL); // TODO: should probably check if the sRGB color space chunk is present isSRGB = true; return loaded; } void Image::MultiplyAlpha() { if (pixelFormat == PixelFormat::R || pixelFormat == PixelFormat::RGB) { return; } else if (pixelFormat == PixelFormat::RA) { size_t pixelSize = 2; for (size_t i = 0; i < width * height; i++) { float red = (float)data[i * pixelSize] / 255.0f; float alpha = (float)data[i * pixelSize + 1] / 255.0f; data[i * pixelSize] = (uint8_t)((red * alpha) * 255.0f); } } else if (pixelFormat == PixelFormat::RGBA) { size_t pixelSize = 4; for (size_t i = 0; i < width * height; i++) { float red = (float)data[i * pixelSize] / 255.0f; float green = (float)data[i * pixelSize + 1] / 255.0f; float blue = (float)data[i * pixelSize + 2] / 255.0f; float alpha = (float)data[i * pixelSize + 3] / 255.0f; data[i * pixelSize] = (uint8_t)((red * alpha) * 255.0f); data[i * pixelSize + 1] = (uint8_t)((green * alpha) * 255.0f); data[i * pixelSize + 2] = (uint8_t)((blue * alpha) * 255.0f); } } } ================================================ FILE: src/core/image.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include enum class PixelFormat { R = 0, // intensity RA, // intensity alpha RGB, RGBA }; struct Image { Image(); bool Load(const std::string& filename); void MultiplyAlpha(); uint32_t width; uint32_t height; PixelFormat pixelFormat; bool isSRGB; std::vector data; }; ================================================ FILE: src/core/inputbuddy.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "inputbuddy.h" #include #include "log.h" InputBuddy::InputBuddy() { int numJoySticks = SDL_NumJoysticks(); if (numJoySticks > 0) { SDL_Joystick* joystick = SDL_JoystickOpen(0); Log::I("Found joystick \"%s\"\n", SDL_JoystickName(joystick)); } else { Log::I("No joystick found\n"); } } void InputBuddy::ProcessEvent(const SDL_Event& event) { switch (event.type) { case SDL_QUIT: quitCallback(); break; case SDL_KEYDOWN: case SDL_KEYUP: { SDL_Keycode keycode = event.key.keysym.sym; uint16_t mod = event.key.keysym.mod; bool down = (event.key.type == SDL_KEYDOWN); auto iter = keyCallbackMap.find(keycode); if (iter != keyCallbackMap.end() && !event.key.repeat) { iter->second(down, mod); } } break; case SDL_JOYAXISMOTION: UpdateJoypadAxis(event.jaxis); break; case SDL_JOYHATMOTION: UpdateJoypadHat(event.jhat); break; case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP: UpdateJoypadButton(event.jbutton); break; case SDL_WINDOWEVENT: if (event.window.event == SDL_WINDOWEVENT_RESIZED) { resizeCallback(event.window.data1, event.window.data2); } break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: if (event.button.clicks == 1) { mouseButtonCallback(event.button.button, event.button.state == SDL_PRESSED, glm::ivec2(event.button.x, event.button.y)); } break; case SDL_MOUSEMOTION: mouseMotionCallback(glm::ivec2(event.motion.x, event.motion.y), glm::ivec2(event.motion.xrel, event.motion.yrel)); break; } } void InputBuddy::OnKey(Keycode key, const KeyCallback& cb) { keyCallbackMap.insert(std::pair(key, cb)); } void InputBuddy::OnQuit(const VoidCallback& cb) { quitCallback = cb; } void InputBuddy::OnResize(const ResizeCallback& cb) { resizeCallback = cb; } void InputBuddy::OnMouseButton(const MouseButtonCallback& cb) { mouseButtonCallback = cb; } void InputBuddy::OnMouseMotion(const MouseMotionCallback& cb) { mouseMotionCallback = cb; } void InputBuddy::SetRelativeMouseMode(bool val) { SDL_SetRelativeMouseMode(val ? SDL_TRUE : SDL_FALSE); } const uint8_t LEFT_STICK_X_AXIS = 0; const uint8_t LEFT_STICK_Y_AXIS = 1; const uint8_t RIGHT_STICK_X_AXIS = 2; const uint8_t RIGHT_STICK_Y_AXIS = 3; const uint8_t LEFT_TRIGGER_AXIS = 4; const uint8_t RIGHT_TRIGGER_AXIS = 5; static float Deadspot(float v) { const float DEADSPOT = 0.15f; return fabs(v) > DEADSPOT ? v : 0.0f; } void InputBuddy::UpdateJoypadAxis(const SDL_JoyAxisEvent& event) { // only support one joypad if (event.which != 0) { return; } const float AXIS_MAX = (float)SDL_JOYSTICK_AXIS_MAX; switch (event.axis) { case LEFT_STICK_X_AXIS: joypad.leftStick.x = Deadspot(event.value / AXIS_MAX); break; case LEFT_STICK_Y_AXIS: joypad.leftStick.y = Deadspot(-event.value / AXIS_MAX); break; case RIGHT_STICK_X_AXIS: joypad.rightStick.x = Deadspot(event.value / AXIS_MAX); break; case RIGHT_STICK_Y_AXIS: joypad.rightStick.y = Deadspot(-event.value / AXIS_MAX); break; case LEFT_TRIGGER_AXIS: joypad.leftTrigger = ((event.value / AXIS_MAX) * 0.5f) + 0.5f; // (0, 1) break; case RIGHT_TRIGGER_AXIS: joypad.rightTrigger = ((event.value / AXIS_MAX) * 0.5f) + 0.5f; // (0, 1) break; } } const uint8_t DPAD_HAT = 0; void InputBuddy::UpdateJoypadHat(const SDL_JoyHatEvent& event) { // only support one joypad if (event.which != 0) { return; } switch (event.hat) { case DPAD_HAT: joypad.up = (event.value & SDL_HAT_UP) ? true : false; joypad.right = (event.value & SDL_HAT_RIGHT) ? true : false; joypad.down = (event.value & SDL_HAT_DOWN) ? true : false; joypad.left = (event.value & SDL_HAT_LEFT) ? true : false; break; } } const uint8_t A_BUTTON = 0; const uint8_t B_BUTTON = 1; const uint8_t X_BUTTON = 2; const uint8_t Y_BUTTON = 3; const uint8_t LEFT_BUMPER_BUTTON = 4; const uint8_t RIGHT_BUMPER_BUTTON = 5; const uint8_t MENU_BUTTON = 6; const uint8_t VIEW_BUTTON = 7; const uint8_t LEFT_STICK_BUTTON = 8; const uint8_t RIGHT_STICK_BUTTON = 9; void InputBuddy::UpdateJoypadButton(const SDL_JoyButtonEvent& event) { // only support one joypad if (event.which != 0) { return; } switch (event.button) { case A_BUTTON: joypad.a = event.state ? true : false; break; case B_BUTTON: joypad.b = event.state ? true : false; break; case X_BUTTON: joypad.x = event.state ? true : false; break; case Y_BUTTON: joypad.y = event.state ? true : false; break; case LEFT_BUMPER_BUTTON: joypad.lb = event.state ? true : false; break; case RIGHT_BUMPER_BUTTON: joypad.rb = event.state ? true : false; break; case MENU_BUTTON: joypad.menu = event.state ? true : false; break; case VIEW_BUTTON: joypad.view = event.state ? true : false; break; case LEFT_STICK_BUTTON: joypad.ls = event.state ? true : false; break; case RIGHT_STICK_BUTTON: joypad.rs = event.state ? true : false; break; } } ================================================ FILE: src/core/inputbuddy.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include union SDL_Event; struct SDL_JoyAxisEvent; struct SDL_JoyHatEvent; struct SDL_JoyButtonEvent; class InputBuddy { public: using Keycode = int32_t; InputBuddy(); using VoidCallback = std::function; using KeyCallback = std::function; using ResizeCallback = std::function; using MouseButtonCallback = std::function; // button 1 = LEFT, 2 = MIDDLE, 3 = RIGHT using MouseMotionCallback = std::function; void ProcessEvent(const SDL_Event& event); void OnKey(Keycode key, const KeyCallback& cb); void OnQuit(const VoidCallback& cb); void OnResize(const ResizeCallback& cb); void OnMouseButton(const MouseButtonCallback& cb); void OnMouseMotion(const MouseMotionCallback& cb); void SetRelativeMouseMode(bool val); // based on an xbox controler struct Joypad { Joypad() { memset(this, 0, sizeof(Joypad)); } glm::vec2 leftStick; glm::vec2 rightStick; float leftTrigger; float rightTrigger; bool down:1; bool up:1; bool left:1; bool right:1; bool view:1; bool menu:1; bool rs:1; bool ls:1; bool lb:1; bool rb:1; bool a:1; bool b:1; bool x:1; bool y:1; }; const Joypad& GetJoypad() const { return joypad; } protected: void UpdateJoypadAxis(const SDL_JoyAxisEvent& event); void UpdateJoypadHat(const SDL_JoyHatEvent& event); void UpdateJoypadButton(const SDL_JoyButtonEvent& event); std::map keyCallbackMap; VoidCallback quitCallback; ResizeCallback resizeCallback; MouseButtonCallback mouseButtonCallback; MouseMotionCallback mouseMotionCallback; Joypad joypad; }; ================================================ FILE: src/core/log.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "log.h" #include #include #ifdef __ANDROID__ #include #endif static Log::LogLevel level = Log::Verbose; static std::string appName = "Core"; #ifdef __ANDROID__ static int log_vprintf(int prio, const char *fmt, va_list args) { char buffer[4096]; int rc = vsnprintf(buffer, sizeof(buffer), fmt, args); __android_log_write(prio, appName.c_str(), buffer); return rc; } #else static int log_vprintf(const char *fmt, va_list args) { char buffer[4096]; int rc = vsnprintf(buffer, sizeof(buffer), fmt, args); fwrite(buffer, rc, 1, stdout); fflush(stdout); return rc; } #endif int Log::printf(const char *fmt, ...) { #ifdef __ANDROID__ return 0; #else char buffer[4096]; va_list args; va_start(args, fmt); int rc = vsnprintf(buffer, sizeof(buffer), fmt, args); va_end(args); fwrite(buffer, rc, 1, stdout); fflush(stdout); return rc; #endif } void Log::SetLevel(LogLevel levelIn) { level = levelIn; } void Log::SetAppName(const std::string& appNameIn) { appName = appNameIn; } void Log::V(const char *fmt, ...) { if (level <= Log::Verbose) { Log::printf("[VERBOSE] "); va_list args; va_start(args, fmt); #ifdef __ANDROID__ log_vprintf(ANDROID_LOG_VERBOSE, fmt, args); #else log_vprintf(fmt, args); #endif va_end(args); } } void Log::D(const char *fmt, ...) { if (level <= Log::Debug) { Log::printf("[DEBUG] "); va_list args; va_start(args, fmt); #ifdef __ANDROID__ log_vprintf(ANDROID_LOG_DEBUG, fmt, args); #else log_vprintf(fmt, args); #endif va_end(args); } } void Log::I(const char *fmt, ...) { if (level <= Log::Info) { Log::printf("[INFO] "); va_list args; va_start(args, fmt); #ifdef __ANDROID__ log_vprintf(ANDROID_LOG_INFO, fmt, args); #else log_vprintf(fmt, args); #endif va_end(args); } } void Log::W(const char *fmt, ...) { if (level <= Log::Warning) { Log::printf("[WARNING] "); va_list args; va_start(args, fmt); #ifdef __ANDROID__ log_vprintf(ANDROID_LOG_WARN, fmt, args); #else log_vprintf(fmt, args); #endif va_end(args); } } void Log::E(const char *fmt, ...) { if (level <= Log::Error) { Log::printf("[ERROR] "); va_list args; va_start(args, fmt); #ifdef __ANDROID__ log_vprintf(ANDROID_LOG_ERROR, fmt, args); #else log_vprintf(fmt, args); #endif va_end(args); } } ================================================ FILE: src/core/log.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include struct Log { enum LogLevel { Verbose = 0, Debug = 1, Info = 2, Warning = 3, Error = 4 }; static void SetLevel(LogLevel levelIn); static void SetAppName(const std::string& appNameIn); // verbose static void V(const char *fmt, ...); // debug static void D(const char *fmt, ...); // info static void I(const char *fmt, ...); // warning static void W(const char *fmt, ...); // error static void E(const char *fmt, ...); private: static int printf(const char *fmt, ...); }; ================================================ FILE: src/core/optionparser.h ================================================ /* * The Lean Mean C++ Option Parser * * Copyright (C) 2012-2017 Matthias S. Benkmann * * The "Software" in the following 2 paragraphs refers to this file containing * the code to The Lean Mean C++ Option Parser. * The "Software" does NOT refer to any other files which you * may have received alongside this file (e.g. as part of a larger project that * incorporates The Lean Mean C++ Option Parser). * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software, to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit * persons to whom the Software is furnished to do so, subject to the following * conditions: * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /* * NOTE: It is recommended that you read the processed HTML doxygen documentation * rather than this source. If you don't know doxygen, it's like javadoc for C++. * If you don't want to install doxygen you can find a copy of the processed * documentation at * * http://optionparser.sourceforge.net/ * */ /** * @file * * @brief This is the only file required to use The Lean Mean C++ Option Parser. * Just \#include it and you're set. * * The Lean Mean C++ Option Parser handles the program's command line arguments * (argc, argv). * It supports the short and long option formats of getopt(), getopt_long() * and getopt_long_only() but has a more convenient interface. * * @par Feedback: * Send questions, bug reports, feature requests etc. to: optionparser-feedback(a)lists.sourceforge.net * * @par Highlights: *

    *
  • It is a header-only library. Just \#include "optionparser.h" and you're set. *
  • It is freestanding. There are no dependencies whatsoever, not even the * C or C++ standard library. *
  • It has a usage message formatter that supports column alignment and * line wrapping. This aids localization because it adapts to * translated strings that are shorter or longer (even if they contain * Asian wide characters). *
  • Unlike getopt() and derivatives it doesn't force you to loop through * options sequentially. Instead you can access options directly like this: *
      *
    • Test for presence of a switch in the argument vector: * @code if ( options[QUIET] ) ... @endcode *
    • Evaluate --enable-foo/--disable-foo pair where the last one used wins: * @code if ( options[FOO].last()->type() == DISABLE ) ... @endcode *
    • Cumulative option (-v verbose, -vv more verbose, -vvv even more verbose): * @code int verbosity = options[VERBOSE].count(); @endcode *
    • Iterate over all --file=<fname> arguments: * @code for (Option* opt = options[FILE]; opt; opt = opt->next()) * fname = opt->arg; ... @endcode *
    • If you really want to, you can still process all arguments in order: * @code * for (int i = 0; i < p.optionsCount(); ++i) { * Option& opt = buffer[i]; * switch(opt.index()) { * case HELP: ... * case VERBOSE: ... * case FILE: fname = opt.arg; ... * case UNKNOWN: ... * @endcode *
    *
@n * Despite these features the code size remains tiny. * It is smaller than uClibc's GNU getopt() and just a * couple 100 bytes larger than uClibc's SUSv3 getopt(). @n * (This does not include the usage formatter, of course. But you don't have to use that.) * * @par Download: * Tarball with examples and test programs: * optionparser-1.7.tar.gz @n * Just the header (this is all you really need): * optionparser.h * * @par Changelog: * Version 1.7: Work on const-correctness. @n * Version 1.6: Fix for MSC compiler. @n * Version 1.5: Fixed 2 warnings about potentially uninitialized variables. @n * Added const version of Option::next(). @n * Version 1.4: Fixed 2 printUsage() bugs that messed up output with small COLUMNS values. @n * Version 1.3: Compatible with Microsoft Visual C++. @n * Version 1.2: Added @ref option::Option::namelen "Option::namelen" and removed the extraction * of short option characters into a special buffer. @n * Changed @ref option::Arg::Optional "Arg::Optional" to accept arguments if they are attached * rather than separate. This is what GNU getopt() does and how POSIX recommends * utilities should interpret their arguments.@n * Version 1.1: Optional mode with argument reordering as done by GNU getopt(), so that * options and non-options can be mixed. See * @ref option::Parser::parse() "Parser::parse()". * * * @par Example program: * (Note: @c option::* identifiers are links that take you to their documentation.) * @code * #error EXAMPLE SHORTENED FOR READABILITY. BETTER EXAMPLES ARE IN THE .TAR.GZ! * #include * #include "optionparser.h" * * enum optionIndex { UNKNOWN, HELP, PLUS }; * const option::Descriptor usage[] = * { * {UNKNOWN, 0,"" , "" ,option::Arg::None, "USAGE: example [options]\n\n" * "Options:" }, * {HELP, 0,"" , "help",option::Arg::None, " --help \tPrint usage and exit." }, * {PLUS, 0,"p", "plus",option::Arg::None, " --plus, -p \tIncrement count." }, * {UNKNOWN, 0,"" , "" ,option::Arg::None, "\nExamples:\n" * " example --unknown -- --this_is_no_option\n" * " example -unk --plus -ppp file1 file2\n" }, * {0,0,0,0,0,0} * }; * * int main(int argc, char* argv[]) * { * argc-=(argc>0); argv+=(argc>0); // skip program name argv[0] if present * option::Stats stats(usage, argc, argv); * option::Option options[stats.options_max], buffer[stats.buffer_max]; * option::Parser parse(usage, argc, argv, options, buffer); * * if (parse.error()) * return 1; * * if (options[HELP] || argc == 0) { * option::printUsage(std::cout, usage); * return 0; * } * * std::cout << "--plus count: " << * options[PLUS].count() << "\n"; * * for (option::Option* opt = options[UNKNOWN]; opt; opt = opt->next()) * std::cout << "Unknown option: " << opt->name << "\n"; * * for (int i = 0; i < parse.nonOptionsCount(); ++i) * std::cout << "Non-option #" << i << ": " << parse.nonOption(i) << "\n"; * } * @endcode * * @par Option syntax: * @li The Lean Mean C++ Option Parser follows POSIX getopt() conventions and supports * GNU-style getopt_long() long options as well as Perl-style single-minus * long options (getopt_long_only()). * @li short options have the format @c -X where @c X is any character that fits in a char. * @li short options can be grouped, i.e. -X -Y is equivalent to @c -XY. * @li a short option may take an argument either separate (-X foo) or * attached (@c -Xfoo). You can make the parser accept the additional format @c -X=foo by * registering @c X as a long option (in addition to being a short option) and * enabling single-minus long options. * @li an argument-taking short option may be grouped if it is the last in the group, e.g. * @c -ABCXfoo or -ABCX foo (@c foo is the argument to the @c -X option). * @li a lone minus character @c '-' is not treated as an option. It is customarily used where * a file name is expected to refer to stdin or stdout. * @li long options have the format @c --option-name. * @li the option-name of a long option can be anything and include any characters. * Even @c = characters will work, but don't do that. * @li [optional] long options may be abbreviated as long as the abbreviation is unambiguous. * You can set a minimum length for abbreviations. * @li [optional] long options may begin with a single minus. The double minus form is always * accepted, too. * @li a long option may take an argument either separate ( --option arg ) or * attached ( --option=arg ). In the attached form the equals sign is mandatory. * @li an empty string can be passed as an attached long option argument: --option-name= . * Note the distinction between an empty string as argument and no argument at all. * @li an empty string is permitted as separate argument to both long and short options. * @li Arguments to both short and long options may start with a @c '-' character. E.g. * -X-X , -X -X or --long-X=-X . If @c -X * and @c --long-X take an argument, that argument will be @c "-X" in all 3 cases. * @li If using the built-in @ref option::Arg::Optional "Arg::Optional", optional arguments must * be attached. * @li the special option @c -- (i.e. without a name) terminates the list of * options. Everything that follows is a non-option argument, even if it starts with * a @c '-' character. The @c -- itself will not appear in the parse results. * @li the first argument that doesn't start with @c '-' or @c '--' and does not belong to * a preceding argument-taking option, will terminate the option list and is the * first non-option argument. All following command line arguments are treated as * non-option arguments, even if they start with @c '-' . @n * NOTE: This behaviour is mandated by POSIX, but GNU getopt() only honours this if it is * explicitly requested (e.g. by setting POSIXLY_CORRECT). @n * You can enable the GNU behaviour by passing @c true as first argument to * e.g. @ref option::Parser::parse() "Parser::parse()". * @li Arguments that look like options (i.e. @c '-' followed by at least 1 character) but * aren't, are NOT treated as non-option arguments. They are treated as unknown options and * are collected into a list of unknown options for error reporting. @n * This means that in order to pass a first non-option * argument beginning with the minus character it is required to use the * @c -- special option, e.g. * @code * program -x -- --strange-filename * @endcode * In this example, @c --strange-filename is a non-option argument. If the @c -- * were omitted, it would be treated as an unknown option. @n * See @ref option::Descriptor::longopt for information on how to collect unknown options. * */ #ifndef OPTIONPARSER_H_ #define OPTIONPARSER_H_ #ifdef _MSC_VER #include #pragma intrinsic(_BitScanReverse) #endif /** @brief The namespace of The Lean Mean C++ Option Parser. */ namespace option { #ifdef _MSC_VER struct MSC_Builtin_CLZ { static int builtin_clz(unsigned x) { unsigned long index; _BitScanReverse(&index, x); return 32-index; // int is always 32bit on Windows, even for target x64 } }; #define __builtin_clz(x) MSC_Builtin_CLZ::builtin_clz(x) #endif class Option; /** * @brief Possible results when checking if an argument is valid for a certain option. * * In the case that no argument is provided for an option that takes an * optional argument, return codes @c ARG_OK and @c ARG_IGNORE are equivalent. */ enum ArgStatus { //! The option does not take an argument. ARG_NONE, //! The argument is acceptable for the option. ARG_OK, //! The argument is not acceptable but that's non-fatal because the option's argument is optional. ARG_IGNORE, //! The argument is not acceptable and that's fatal. ARG_ILLEGAL }; /** * @brief Signature of functions that check if an argument is valid for a certain type of option. * * Every Option has such a function assigned in its Descriptor. * @code * Descriptor usage[] = { {UNKNOWN, 0, "", "", Arg::None, ""}, ... }; * @endcode * * A CheckArg function has the following signature: * @code ArgStatus CheckArg(const Option& option, bool msg); @endcode * * It is used to check if a potential argument would be acceptable for the option. * It will even be called if there is no argument. In that case @c option.arg will be @c NULL. * * If @c msg is @c true and the function determines that an argument is not acceptable and * that this is a fatal error, it should output a message to the user before * returning @ref ARG_ILLEGAL. If @c msg is @c false the function should remain silent (or you * will get duplicate messages). * * See @ref ArgStatus for the meaning of the return values. * * While you can provide your own functions, * often the following pre-defined checks (which never return @ref ARG_ILLEGAL) will suffice: * * @li @c Arg::None @copybrief Arg::None * @li @c Arg::Optional @copybrief Arg::Optional * */ typedef ArgStatus (*CheckArg)(const Option& option, bool msg); /** * @brief Describes an option, its help text (usage) and how it should be parsed. * * The main input when constructing an option::Parser is an array of Descriptors. * @par Example: * @code * enum OptionIndex {CREATE, ...}; * enum OptionType {DISABLE, ENABLE, OTHER}; * * const option::Descriptor usage[] = { * { CREATE, // index * OTHER, // type * "c", // shortopt * "create", // longopt * Arg::None, // check_arg * "--create Tells the program to create something." // help * } * , ... * }; * @endcode */ struct Descriptor { /** * @brief Index of this option's linked list in the array filled in by the parser. * * Command line options whose Descriptors have the same index will end up in the same * linked list in the order in which they appear on the command line. If you have * multiple long option aliases that refer to the same option, give their descriptors * the same @c index. * * If you have options that mean exactly opposite things * (e.g. @c --enable-foo and @c --disable-foo ), you should also give them the same * @c index, but distinguish them through different values for @ref type. * That way they end up in the same list and you can just take the last element of the * list and use its type. This way you get the usual behaviour where switches later * on the command line override earlier ones without having to code it manually. * * @par Tip: * Use an enum rather than plain ints for better readability, as shown in the example * at Descriptor. */ const unsigned index; /** * @brief Used to distinguish between options with the same @ref index. * See @ref index for details. * * It is recommended that you use an enum rather than a plain int to make your * code more readable. */ const int type; /** * @brief Each char in this string will be accepted as a short option character. * * The string must not include the minus character @c '-' or you'll get undefined * behaviour. * * If this Descriptor should not have short option characters, use the empty * string "". NULL is not permitted here! * * See @ref longopt for more information. */ const char* const shortopt; /** * @brief The long option name (without the leading @c -- ). * * If this Descriptor should not have a long option name, use the empty * string "". NULL is not permitted here! * * While @ref shortopt allows multiple short option characters, each * Descriptor can have only a single long option name. If you have multiple * long option names referring to the same option use separate Descriptors * that have the same @ref index and @ref type. You may repeat * short option characters in such an alias Descriptor but there's no need to. * * @par Dummy Descriptors: * You can use dummy Descriptors with an * empty string for both @ref shortopt and @ref longopt to add text to * the usage that is not related to a specific option. See @ref help. * The first dummy Descriptor will be used for unknown options (see below). * * @par Unknown Option Descriptor: * The first dummy Descriptor in the list of Descriptors, * whose @ref shortopt and @ref longopt are both the empty string, will be used * as the Descriptor for unknown options. An unknown option is a string in * the argument vector that is not a lone minus @c '-' but starts with a minus * character and does not match any Descriptor's @ref shortopt or @ref longopt. @n * Note that the dummy descriptor's @ref check_arg function @e will be called and * its return value will be evaluated as usual. I.e. if it returns @ref ARG_ILLEGAL * the parsing will be aborted with Parser::error()==true. @n * if @c check_arg does not return @ref ARG_ILLEGAL the descriptor's * @ref index @e will be used to pick the linked list into which * to put the unknown option. @n * If there is no dummy descriptor, unknown options will be dropped silently. * */ const char* const longopt; /** * @brief For each option that matches @ref shortopt or @ref longopt this function * will be called to check a potential argument to the option. * * This function will be called even if there is no potential argument. In that case * it will be passed @c NULL as @c arg parameter. Do not confuse this with the empty * string. * * See @ref CheckArg for more information. */ const CheckArg check_arg; /** * @brief The usage text associated with the options in this Descriptor. * * You can use option::printUsage() to format your usage message based on * the @c help texts. You can use dummy Descriptors where * @ref shortopt and @ref longopt are both the empty string to add text to * the usage that is not related to a specific option. * * See option::printUsage() for special formatting characters you can use in * @c help to get a column layout. * * @attention * Must be UTF-8-encoded. If your compiler supports C++11 you can use the "u8" * prefix to make sure string literals are properly encoded. */ const char* help; }; /** * @brief A parsed option from the command line together with its argument if it has one. * * The Parser chains all parsed options with the same Descriptor::index together * to form a linked list. This allows you to easily implement all of the common ways * of handling repeated options and enable/disable pairs. * * @li Test for presence of a switch in the argument vector: * @code if ( options[QUIET] ) ... @endcode * @li Evaluate --enable-foo/--disable-foo pair where the last one used wins: * @code if ( options[FOO].last()->type() == DISABLE ) ... @endcode * @li Cumulative option (-v verbose, -vv more verbose, -vvv even more verbose): * @code int verbosity = options[VERBOSE].count(); @endcode * @li Iterate over all --file=<fname> arguments: * @code for (Option* opt = options[FILE]; opt; opt = opt->next()) * fname = opt->arg; ... @endcode */ class Option { Option* next_; Option* prev_; public: /** * @brief Pointer to this Option's Descriptor. * * Remember that the first dummy descriptor (see @ref Descriptor::longopt) is used * for unknown options. * * @attention * @c desc==NULL signals that this Option is unused. This is the default state of * elements in the result array. You don't need to test @c desc explicitly. You * can simply write something like this: * @code * if (options[CREATE]) * { * ... * } * @endcode * This works because of operator const Option*() . */ const Descriptor* desc; /** * @brief The name of the option as used on the command line. * * The main purpose of this string is to be presented to the user in messages. * * In the case of a long option, this is the actual @c argv pointer, i.e. the first * character is a '-'. In the case of a short option this points to the option * character within the @c argv string. * * Note that in the case of a short option group or an attached option argument, this * string will contain additional characters following the actual name. Use @ref namelen * to filter out the actual option name only. * */ const char* name; /** * @brief Pointer to this Option's argument (if any). * * NULL if this option has no argument. Do not confuse this with the empty string which * is a valid argument. */ const char* arg; /** * @brief The length of the option @ref name. * * Because @ref name points into the actual @c argv string, the option name may be * followed by more characters (e.g. other short options in the same short option group). * This value is the number of bytes (not characters!) that are part of the actual name. * * For a short option, this length is always 1. For a long option this length is always * at least 2 if single minus long options are permitted and at least 3 if they are disabled. * * @note * In the pathological case of a minus within a short option group (e.g. @c -xf-z), this * length is incorrect, because this case will be misinterpreted as a long option and the * name will therefore extend to the string's 0-terminator or a following '=" character * if there is one. This is irrelevant for most uses of @ref name and @c namelen. If you * really need to distinguish the case of a long and a short option, compare @ref name to * the @c argv pointers. A long option's @c name is always identical to one of them, * whereas a short option's is never. */ int namelen; /** * @brief Returns Descriptor::type of this Option's Descriptor, or 0 if this Option * is invalid (unused). * * Because this method (and last(), too) can be used even on unused Options with desc==0, you can (provided * you arrange your types properly) switch on type() without testing validity first. * @code * enum OptionType { UNUSED=0, DISABLED=0, ENABLED=1 }; * enum OptionIndex { FOO }; * const Descriptor usage[] = { * { FOO, ENABLED, "", "enable-foo", Arg::None, 0 }, * { FOO, DISABLED, "", "disable-foo", Arg::None, 0 }, * { 0, 0, 0, 0, 0, 0 } }; * ... * switch(options[FOO].last()->type()) // no validity check required! * { * case ENABLED: ... * case DISABLED: ... // UNUSED==DISABLED ! * } * @endcode */ int type() const { return desc == 0 ? 0 : desc->type; } /** * @brief Returns Descriptor::index of this Option's Descriptor, or -1 if this Option * is invalid (unused). */ int index() const { return desc == 0 ? -1 : (int)desc->index; } /** * @brief Returns the number of times this Option (or others with the same Descriptor::index) * occurs in the argument vector. * * This corresponds to the number of elements in the linked list this Option is part of. * It doesn't matter on which element you call count(). The return value is always the same. * * Use this to implement cumulative options, such as -v, -vv, -vvv for * different verbosity levels. * * Returns 0 when called for an unused/invalid option. */ int count() const { int c = (desc == 0 ? 0 : 1); const Option* p = first(); while (!p->isLast()) { ++c; p = p->next_; }; return c; } /** * @brief Returns true iff this is the first element of the linked list. * * The first element in the linked list is the first option on the command line * that has the respective Descriptor::index value. * * Returns true for an unused/invalid option. */ bool isFirst() const { return isTagged(prev_); } /** * @brief Returns true iff this is the last element of the linked list. * * The last element in the linked list is the last option on the command line * that has the respective Descriptor::index value. * * Returns true for an unused/invalid option. */ bool isLast() const { return isTagged(next_); } /** * @brief Returns a pointer to the first element of the linked list. * * Use this when you want the first occurrence of an option on the command line to * take precedence. Note that this is not the way most programs handle options. * You should probably be using last() instead. * * @note * This method may be called on an unused/invalid option and will return a pointer to the * option itself. */ Option* first() { Option* p = this; while (!p->isFirst()) p = p->prev_; return p; } /** * const version of Option::first(). */ const Option* first() const { return const_cast(this)->first(); } /** * @brief Returns a pointer to the last element of the linked list. * * Use this when you want the last occurrence of an option on the command line to * take precedence. This is the most common way of handling conflicting options. * * @note * This method may be called on an unused/invalid option and will return a pointer to the * option itself. * * @par Tip: * If you have options with opposite meanings (e.g. @c --enable-foo and @c --disable-foo), you * can assign them the same Descriptor::index to get them into the same list. Distinguish them by * Descriptor::type and all you have to do is check last()->type() to get * the state listed last on the command line. */ Option* last() { return first()->prevwrap(); } /** * const version of Option::last(). */ const Option* last() const { return first()->prevwrap(); } /** * @brief Returns a pointer to the previous element of the linked list or NULL if * called on first(). * * If called on first() this method returns NULL. Otherwise it will return the * option with the same Descriptor::index that precedes this option on the command * line. */ Option* prev() { return isFirst() ? 0 : prev_; } /** * @brief Returns a pointer to the previous element of the linked list with wrap-around from * first() to last(). * * If called on first() this method returns last(). Otherwise it will return the * option with the same Descriptor::index that precedes this option on the command * line. */ Option* prevwrap() { return untag(prev_); } /** * const version of Option::prevwrap(). */ const Option* prevwrap() const { return untag(prev_); } /** * @brief Returns a pointer to the next element of the linked list or NULL if called * on last(). * * If called on last() this method returns NULL. Otherwise it will return the * option with the same Descriptor::index that follows this option on the command * line. */ Option* next() { return isLast() ? 0 : next_; } /** * const version of Option::next(). */ const Option* next() const { return isLast() ? 0 : next_; } /** * @brief Returns a pointer to the next element of the linked list with wrap-around from * last() to first(). * * If called on last() this method returns first(). Otherwise it will return the * option with the same Descriptor::index that follows this option on the command * line. */ Option* nextwrap() { return untag(next_); } /** * @brief Makes @c new_last the new last() by chaining it into the list after last(). * * It doesn't matter which element you call append() on. The new element will always * be appended to last(). * * @attention * @c new_last must not yet be part of a list, or that list will become corrupted, because * this method does not unchain @c new_last from an existing list. */ void append(Option* new_last) { Option* p = last(); Option* f = first(); p->next_ = new_last; new_last->prev_ = p; new_last->next_ = tag(f); f->prev_ = tag(new_last); } /** * @brief Casts from Option to const Option* but only if this Option is valid. * * If this Option is valid (i.e. @c desc!=NULL), returns this. * Otherwise returns NULL. This allows testing an Option directly * in an if-clause to see if it is used: * @code * if (options[CREATE]) * { * ... * } * @endcode * It also allows you to write loops like this: * @code for (Option* opt = options[FILE]; opt; opt = opt->next()) * fname = opt->arg; ... @endcode */ operator const Option*() const { return desc ? this : 0; } /** * @brief Casts from Option to Option* but only if this Option is valid. * * If this Option is valid (i.e. @c desc!=NULL), returns this. * Otherwise returns NULL. This allows testing an Option directly * in an if-clause to see if it is used: * @code * if (options[CREATE]) * { * ... * } * @endcode * It also allows you to write loops like this: * @code for (Option* opt = options[FILE]; opt; opt = opt->next()) * fname = opt->arg; ... @endcode */ operator Option*() { return desc ? this : 0; } /** * @brief Creates a new Option that is a one-element linked list and has NULL * @ref desc, @ref name, @ref arg and @ref namelen. */ Option() : desc(0), name(0), arg(0), namelen(0) { prev_ = tag(this); next_ = tag(this); } /** * @brief Creates a new Option that is a one-element linked list and has the given * values for @ref desc, @ref name and @ref arg. * * If @c name_ points at a character other than '-' it will be assumed to refer to a * short option and @ref namelen will be set to 1. Otherwise the length will extend to * the first '=' character or the string's 0-terminator. */ Option(const Descriptor* desc_, const char* name_, const char* arg_) { init(desc_, name_, arg_); } /** * @brief Makes @c *this a copy of @c orig except for the linked list pointers. * * After this operation @c *this will be a one-element linked list. */ void operator=(const Option& orig) { init(orig.desc, orig.name, orig.arg); } /** * @brief Makes @c *this a copy of @c orig except for the linked list pointers. * * After this operation @c *this will be a one-element linked list. */ Option(const Option& orig) { init(orig.desc, orig.name, orig.arg); } private: /** * @internal * @brief Sets the fields of this Option to the given values (extracting @c name if necessary). * * If @c name_ points at a character other than '-' it will be assumed to refer to a * short option and @ref namelen will be set to 1. Otherwise the length will extend to * the first '=' character or the string's 0-terminator. */ void init(const Descriptor* desc_, const char* name_, const char* arg_) { desc = desc_; name = name_; arg = arg_; prev_ = tag(this); next_ = tag(this); namelen = 0; if (name == 0) return; namelen = 1; if (name[0] != '-') return; while (name[namelen] != 0 && name[namelen] != '=') ++namelen; } static Option* tag(Option* ptr) { return (Option*) ((unsigned long long) ptr | 1); } static Option* untag(Option* ptr) { return (Option*) ((unsigned long long) ptr & ~1ull); } static bool isTagged(Option* ptr) { return ((unsigned long long) ptr & 1); } }; /** * @brief Functions for checking the validity of option arguments. * * @copydetails CheckArg * * The following example code * can serve as starting place for writing your own more complex CheckArg functions: * @code * struct Arg: public option::Arg * { * static void printError(const char* msg1, const option::Option& opt, const char* msg2) * { * fprintf(stderr, "ERROR: %s", msg1); * fwrite(opt.name, opt.namelen, 1, stderr); * fprintf(stderr, "%s", msg2); * } * * static option::ArgStatus Unknown(const option::Option& option, bool msg) * { * if (msg) printError("Unknown option '", option, "'\n"); * return option::ARG_ILLEGAL; * } * * static option::ArgStatus Required(const option::Option& option, bool msg) * { * if (option.arg != 0) * return option::ARG_OK; * * if (msg) printError("Option '", option, "' requires an argument\n"); * return option::ARG_ILLEGAL; * } * * static option::ArgStatus NonEmpty(const option::Option& option, bool msg) * { * if (option.arg != 0 && option.arg[0] != 0) * return option::ARG_OK; * * if (msg) printError("Option '", option, "' requires a non-empty argument\n"); * return option::ARG_ILLEGAL; * } * * static option::ArgStatus Numeric(const option::Option& option, bool msg) * { * char* endptr = 0; * if (option.arg != 0 && strtol(option.arg, &endptr, 10)){}; * if (endptr != option.arg && *endptr == 0) * return option::ARG_OK; * * if (msg) printError("Option '", option, "' requires a numeric argument\n"); * return option::ARG_ILLEGAL; * } * }; * @endcode */ struct Arg { //! @brief For options that don't take an argument: Returns ARG_NONE. static ArgStatus None(const Option&, bool) { return ARG_NONE; } //! @brief Returns ARG_OK if the argument is attached and ARG_IGNORE otherwise. static ArgStatus Optional(const Option& option, bool) { if (option.arg && option.name[option.namelen] != 0) return ARG_OK; else return ARG_IGNORE; } }; /** * @brief Determines the minimum lengths of the buffer and options arrays used for Parser. * * Because Parser doesn't use dynamic memory its output arrays have to be pre-allocated. * If you don't want to use fixed size arrays (which may turn out too small, causing * command line arguments to be dropped), you can use Stats to determine the correct sizes. * Stats work cumulative. You can first pass in your default options and then the real * options and afterwards the counts will reflect the union. */ struct Stats { /** * @brief Number of elements needed for a @c buffer[] array to be used for * @ref Parser::parse() "parsing" the same argument vectors that were fed * into this Stats object. * * @note * This number is always 1 greater than the actual number needed, to give * you a sentinel element. */ unsigned buffer_max; /** * @brief Number of elements needed for an @c options[] array to be used for * @ref Parser::parse() "parsing" the same argument vectors that were fed * into this Stats object. * * @note * @li This number is always 1 greater than the actual number needed, to give * you a sentinel element. * @li This number depends only on the @c usage, not the argument vectors, because * the @c options array needs exactly one slot for each possible Descriptor::index. */ unsigned options_max; /** * @brief Creates a Stats object with counts set to 1 (for the sentinel element). */ Stats() : buffer_max(1), options_max(1) // 1 more than necessary as sentinel { } /** * @brief Creates a new Stats object and immediately updates it for the * given @c usage and argument vector. You may pass 0 for @c argc and/or @c argv, * if you just want to update @ref options_max. * * @note * The calls to Stats methods must match the later calls to Parser methods. * See Parser::parse() for the meaning of the arguments. */ Stats(bool gnu, const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, // bool single_minus_longopt = false) : buffer_max(1), options_max(1) // 1 more than necessary as sentinel { add(gnu, usage, argc, argv, min_abbr_len, single_minus_longopt); } //! @brief Stats(...) with non-const argv. Stats(bool gnu, const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, // bool single_minus_longopt = false) : buffer_max(1), options_max(1) // 1 more than necessary as sentinel { add(gnu, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt); } //! @brief POSIX Stats(...) (gnu==false). Stats(const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, // bool single_minus_longopt = false) : buffer_max(1), options_max(1) // 1 more than necessary as sentinel { add(false, usage, argc, argv, min_abbr_len, single_minus_longopt); } //! @brief POSIX Stats(...) (gnu==false) with non-const argv. Stats(const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, // bool single_minus_longopt = false) : buffer_max(1), options_max(1) // 1 more than necessary as sentinel { add(false, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt); } /** * @brief Updates this Stats object for the * given @c usage and argument vector. You may pass 0 for @c argc and/or @c argv, * if you just want to update @ref options_max. * * @note * The calls to Stats methods must match the later calls to Parser methods. * See Parser::parse() for the meaning of the arguments. */ void add(bool gnu, const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, // bool single_minus_longopt = false); //! @brief add() with non-const argv. void add(bool gnu, const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, // bool single_minus_longopt = false) { add(gnu, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt); } //! @brief POSIX add() (gnu==false). void add(const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, // bool single_minus_longopt = false) { add(false, usage, argc, argv, min_abbr_len, single_minus_longopt); } //! @brief POSIX add() (gnu==false) with non-const argv. void add(const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, // bool single_minus_longopt = false) { add(false, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt); } private: class CountOptionsAction; }; /** * @brief Checks argument vectors for validity and parses them into data * structures that are easier to work with. * * @par Example: * @code * int main(int argc, char* argv[]) * { * argc-=(argc>0); argv+=(argc>0); // skip program name argv[0] if present * option::Stats stats(usage, argc, argv); * option::Option options[stats.options_max], buffer[stats.buffer_max]; * option::Parser parse(usage, argc, argv, options, buffer); * * if (parse.error()) * return 1; * * if (options[HELP]) * ... * @endcode */ class Parser { int op_count; //!< @internal @brief see optionsCount() int nonop_count; //!< @internal @brief see nonOptionsCount() const char** nonop_args; //!< @internal @brief see nonOptions() bool err; //!< @internal @brief see error() public: /** * @brief Creates a new Parser. */ Parser() : op_count(0), nonop_count(0), nonop_args(0), err(false) { } /** * @brief Creates a new Parser and immediately parses the given argument vector. * @copydetails parse() */ Parser(bool gnu, const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[], int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) : op_count(0), nonop_count(0), nonop_args(0), err(false) { parse(gnu, usage, argc, argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax); } //! @brief Parser(...) with non-const argv. Parser(bool gnu, const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[], int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) : op_count(0), nonop_count(0), nonop_args(0), err(false) { parse(gnu, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax); } //! @brief POSIX Parser(...) (gnu==false). Parser(const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[], int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) : op_count(0), nonop_count(0), nonop_args(0), err(false) { parse(false, usage, argc, argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax); } //! @brief POSIX Parser(...) (gnu==false) with non-const argv. Parser(const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[], int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) : op_count(0), nonop_count(0), nonop_args(0), err(false) { parse(false, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax); } /** * @brief Parses the given argument vector. * * @param gnu if true, parse() will not stop at the first non-option argument. Instead it will * reorder arguments so that all non-options are at the end. This is the default behaviour * of GNU getopt() but is not conforming to POSIX. @n * Note, that once the argument vector has been reordered, the @c gnu flag will have * no further effect on this argument vector. So it is enough to pass @c gnu==true when * creating Stats. * @param usage Array of Descriptor objects that describe the options to support. The last entry * of this array must have 0 in all fields. * @param argc The number of elements from @c argv that are to be parsed. If you pass -1, the number * will be determined automatically. In that case the @c argv list must end with a NULL * pointer. * @param argv The arguments to be parsed. If you pass -1 as @c argc the last pointer in the @c argv * list must be NULL to mark the end. * @param options Each entry is the first element of a linked list of Options. Each new option * that is parsed will be appended to the list specified by that Option's * Descriptor::index. If an entry is not yet used (i.e. the Option is invalid), * it will be replaced rather than appended to. @n * The minimum length of this array is the greatest Descriptor::index value that * occurs in @c usage @e PLUS ONE. * @param buffer Each argument that is successfully parsed (including unknown arguments, if they * have a Descriptor whose CheckArg does not return @ref ARG_ILLEGAL) will be stored in this * array. parse() scans the array for the first invalid entry and begins writing at that * index. You can pass @c bufmax to limit the number of options stored. * @param min_abbr_len Passing a value min_abbr_len > 0 enables abbreviated long * options. The parser will match a prefix of a long option as if it was * the full long option (e.g. @c --foob=10 will be interpreted as if it was * @c --foobar=10 ), as long as the prefix has at least @c min_abbr_len characters * (not counting the @c -- ) and is unambiguous. * @n Be careful if combining @c min_abbr_len=1 with @c single_minus_longopt=true * because the ambiguity check does not consider short options and abbreviated * single minus long options will take precedence over short options. * @param single_minus_longopt Passing @c true for this option allows long options to begin with * a single minus. The double minus form will still be recognized. Note that * single minus long options take precedence over short options and short option * groups. E.g. @c -file would be interpreted as @c --file and not as * -f -i -l -e (assuming a long option named @c "file" exists). * @param bufmax The greatest index in the @c buffer[] array that parse() will write to is * @c bufmax-1. If there are more options, they will be processed (in particular * their CheckArg will be called) but not stored. @n * If you used Stats::buffer_max to dimension this array, you can pass * -1 (or not pass @c bufmax at all) which tells parse() that the buffer is * "large enough". * @attention * Remember that @c options and @c buffer store Option @e objects, not pointers. Therefore it * is not possible for the same object to be in both arrays. For those options that are found in * both @c buffer[] and @c options[] the respective objects are independent copies. And only the * objects in @c options[] are properly linked via Option::next() and Option::prev(). * You can iterate over @c buffer[] to * process all options in the order they appear in the argument vector, but if you want access to * the other Options with the same Descriptor::index, then you @e must access the linked list via * @c options[]. You can get the linked list in options from a buffer object via something like * @c options[buffer[i].index()]. */ void parse(bool gnu, const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[], int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1); //! @brief parse() with non-const argv. void parse(bool gnu, const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[], int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) { parse(gnu, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax); } //! @brief POSIX parse() (gnu==false). void parse(const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[], int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) { parse(false, usage, argc, argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax); } //! @brief POSIX parse() (gnu==false) with non-const argv. void parse(const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[], int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) { parse(false, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax); } /** * @brief Returns the number of valid Option objects in @c buffer[]. * * @note * @li The returned value always reflects the number of Options in the buffer[] array used for * the most recent call to parse(). * @li The count (and the buffer[]) includes unknown options if they are collected * (see Descriptor::longopt). */ int optionsCount() { return op_count; } /** * @brief Returns the number of non-option arguments that remained at the end of the * most recent parse() that actually encountered non-option arguments. * * @note * A parse() that does not encounter non-option arguments will leave this value * as well as nonOptions() undisturbed. This means you can feed the Parser a * default argument vector that contains non-option arguments (e.g. a default filename). * Then you feed it the actual arguments from the user. If the user has supplied at * least one non-option argument, all of the non-option arguments from the default * disappear and are replaced by the user's non-option arguments. However, if the * user does not supply any non-option arguments the defaults will still be in * effect. */ int nonOptionsCount() { return nonop_count; } /** * @brief Returns a pointer to an array of non-option arguments (only valid * if nonOptionsCount() >0 ). * * @note * @li parse() does not copy arguments, so this pointer points into the actual argument * vector as passed to parse(). * @li As explained at nonOptionsCount() this pointer is only changed by parse() calls * that actually encounter non-option arguments. A parse() call that encounters only * options, will not change nonOptions(). */ const char** nonOptions() { return nonop_args; } /** * @brief Returns nonOptions()[i] (@e without checking if i is in range!). */ const char* nonOption(int i) { return nonOptions()[i]; } /** * @brief Returns @c true if an unrecoverable error occurred while parsing options. * * An illegal argument to an option (i.e. CheckArg returns @ref ARG_ILLEGAL) is an * unrecoverable error that aborts the parse. Unknown options are only an error if * their CheckArg function returns @ref ARG_ILLEGAL. Otherwise they are collected. * In that case if you want to exit the program if either an illegal argument * or an unknown option has been passed, use code like this * * @code * if (parser.error() || options[UNKNOWN]) * exit(1); * @endcode * */ bool error() { return err; } private: friend struct Stats; class StoreOptionAction; struct Action; /** * @internal * @brief This is the core function that does all the parsing. * @retval false iff an unrecoverable error occurred. */ static bool workhorse(bool gnu, const Descriptor usage[], int numargs, const char** args, Action& action, bool single_minus_longopt, bool print_errors, int min_abbr_len); /** * @internal * @brief Returns true iff @c st1 is a prefix of @c st2 and * in case @c st2 is longer than @c st1, then * the first additional character is '='. * * @par Examples: * @code * streq("foo", "foo=bar") == true * streq("foo", "foobar") == false * streq("foo", "foo") == true * streq("foo=bar", "foo") == false * @endcode */ static bool streq(const char* st1, const char* st2) { while (*st1 != 0) if (*st1++ != *st2++) return false; return (*st2 == 0 || *st2 == '='); } /** * @internal * @brief Like streq() but handles abbreviations. * * Returns true iff @c st1 and @c st2 have a common * prefix with the following properties: * @li (if min > 0) its length is at least @c min characters or the same length as @c st1 (whichever is smaller). * @li (if min <= 0) its length is the same as that of @c st1 * @li within @c st2 the character following the common prefix is either '=' or end-of-string. * * Examples: * @code * streqabbr("foo", "foo=bar",) == true * streqabbr("foo", "fo=bar" , 2) == true * streqabbr("foo", "fo" , 2) == true * streqabbr("foo", "fo" , 0) == false * streqabbr("foo", "f=bar" , 2) == false * streqabbr("foo", "f" , 2) == false * streqabbr("fo" , "foo=bar",) == false * streqabbr("foo", "foobar" ,) == false * streqabbr("foo", "fobar" ,) == false * streqabbr("foo", "foo" ,) == true * @endcode */ static bool streqabbr(const char* st1, const char* st2, long long min) { const char* st1start = st1; while (*st1 != 0 && (*st1 == *st2)) { ++st1; ++st2; } return (*st1 == 0 || (min > 0 && (st1 - st1start) >= min)) && (*st2 == 0 || *st2 == '='); } /** * @internal * @brief Returns true iff character @c ch is contained in the string @c st. * * Returns @c true for @c ch==0 . */ static bool instr(char ch, const char* st) { while (*st != 0 && *st != ch) ++st; return *st == ch; } /** * @internal * @brief Rotates args[-count],...,args[-1],args[0] to become * args[0],args[-count],...,args[-1]. */ static void shift(const char** args, int count) { for (int i = 0; i > -count; --i) { const char* temp = args[i]; args[i] = args[i - 1]; args[i - 1] = temp; } } }; /** * @internal * @brief Interface for actions Parser::workhorse() should perform for each Option it * parses. */ struct Parser::Action { /** * @brief Called by Parser::workhorse() for each Option that has been successfully * parsed (including unknown * options if they have a Descriptor whose Descriptor::check_arg does not return * @ref ARG_ILLEGAL. * * Returns @c false iff a fatal error has occured and the parse should be aborted. */ virtual bool perform(Option&) { return true; } /** * @brief Called by Parser::workhorse() after finishing the parse. * @param numargs the number of non-option arguments remaining * @param args pointer to the first remaining non-option argument (if numargs > 0). * * @return * @c false iff a fatal error has occurred. */ virtual bool finished(int numargs, const char** args) { (void) numargs; (void) args; return true; } }; /** * @internal * @brief An Action to pass to Parser::workhorse() that will increment a counter for * each parsed Option. */ class Stats::CountOptionsAction: public Parser::Action { unsigned* buffer_max; public: /** * Creates a new CountOptionsAction that will increase @c *buffer_max_ for each * parsed Option. */ CountOptionsAction(unsigned* buffer_max_) : buffer_max(buffer_max_) { } bool perform(Option&) { if (*buffer_max == 0x7fffffff) return false; // overflow protection: don't accept number of options that doesn't fit signed int ++*buffer_max; return true; } }; /** * @internal * @brief An Action to pass to Parser::workhorse() that will store each parsed Option in * appropriate arrays (see Parser::parse()). */ class Parser::StoreOptionAction: public Parser::Action { Parser& parser; Option* options; Option* buffer; int bufmax; //! Number of slots in @c buffer. @c -1 means "large enough". public: /** * @brief Creates a new StoreOption action. * @param parser_ the parser whose op_count should be updated. * @param options_ each Option @c o is chained into the linked list @c options_[o.desc->index] * @param buffer_ each Option is appended to this array as long as there's a free slot. * @param bufmax_ number of slots in @c buffer_. @c -1 means "large enough". */ StoreOptionAction(Parser& parser_, Option options_[], Option buffer_[], int bufmax_) : parser(parser_), options(options_), buffer(buffer_), bufmax(bufmax_) { // find first empty slot in buffer (if any) int bufidx = 0; while ((bufmax < 0 || bufidx < bufmax) && buffer[bufidx]) ++bufidx; // set parser's optionCount parser.op_count = bufidx; } bool perform(Option& option) { if (bufmax < 0 || parser.op_count < bufmax) { if (parser.op_count == 0x7fffffff) return false; // overflow protection: don't accept number of options that doesn't fit signed int buffer[parser.op_count] = option; int idx = buffer[parser.op_count].desc->index; if (options[idx]) options[idx].append(buffer[parser.op_count]); else options[idx] = buffer[parser.op_count]; ++parser.op_count; } return true; // NOTE: an option that is discarded because of a full buffer is not fatal } bool finished(int numargs, const char** args) { // only overwrite non-option argument list if there's at least 1 // new non-option argument. Otherwise we keep the old list. This // makes it easy to use default non-option arguments. if (numargs > 0) { parser.nonop_count = numargs; parser.nonop_args = args; } return true; } }; inline void Parser::parse(bool gnu, const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[], int min_abbr_len, bool single_minus_longopt, int bufmax) { StoreOptionAction action(*this, options, buffer, bufmax); err = !workhorse(gnu, usage, argc, argv, action, single_minus_longopt, true, min_abbr_len); } inline void Stats::add(bool gnu, const Descriptor usage[], int argc, const char** argv, int min_abbr_len, bool single_minus_longopt) { // determine size of options array. This is the greatest index used in the usage + 1 int i = 0; while (usage[i].shortopt != 0) { if (usage[i].index + 1 >= options_max) options_max = (usage[i].index + 1) + 1; // 1 more than necessary as sentinel ++i; } CountOptionsAction action(&buffer_max); Parser::workhorse(gnu, usage, argc, argv, action, single_minus_longopt, false, min_abbr_len); } inline bool Parser::workhorse(bool gnu, const Descriptor usage[], int numargs, const char** args, Action& action, bool single_minus_longopt, bool print_errors, int min_abbr_len) { // protect against NULL pointer if (args == 0) numargs = 0; int nonops = 0; while (numargs != 0 && *args != 0) { const char* param = *args; // param can be --long-option, -srto or non-option argument // in POSIX mode the first non-option argument terminates the option list // a lone minus character is a non-option argument if (param[0] != '-' || param[1] == 0) { if (gnu) { ++nonops; ++args; if (numargs > 0) --numargs; continue; } else break; } // -- terminates the option list. The -- itself is skipped. if (param[1] == '-' && param[2] == 0) { shift(args, nonops); ++args; if (numargs > 0) --numargs; break; } bool handle_short_options; const char* longopt_name; if (param[1] == '-') // if --long-option { handle_short_options = false; longopt_name = param + 2; } else { handle_short_options = true; longopt_name = param + 1; //for testing a potential -long-option } bool try_single_minus_longopt = single_minus_longopt; bool have_more_args = (numargs > 1 || numargs < 0); // is referencing argv[1] valid? do // loop over short options in group, for long options the body is executed only once { int idx = 0; const char* optarg = 0; /******************** long option **********************/ if (handle_short_options == false || try_single_minus_longopt) { idx = 0; while (usage[idx].longopt != 0 && !streq(usage[idx].longopt, longopt_name)) ++idx; if (usage[idx].longopt == 0 && min_abbr_len > 0) // if we should try to match abbreviated long options { int i1 = 0; while (usage[i1].longopt != 0 && !streqabbr(usage[i1].longopt, longopt_name, min_abbr_len)) ++i1; if (usage[i1].longopt != 0) { // now test if the match is unambiguous by checking for another match int i2 = i1 + 1; while (usage[i2].longopt != 0 && !streqabbr(usage[i2].longopt, longopt_name, min_abbr_len)) ++i2; if (usage[i2].longopt == 0) // if there was no second match it's unambiguous, so accept i1 as idx idx = i1; } } // if we found something, disable handle_short_options (only relevant if single_minus_longopt) if (usage[idx].longopt != 0) handle_short_options = false; try_single_minus_longopt = false; // prevent looking for longopt in the middle of shortopt group optarg = longopt_name; while (*optarg != 0 && *optarg != '=') ++optarg; if (*optarg == '=') // attached argument ++optarg; else // possibly detached argument optarg = (have_more_args ? args[1] : 0); } /************************ short option ***********************************/ if (handle_short_options) { if (*++param == 0) // point at the 1st/next option character break; // end of short option group idx = 0; while (usage[idx].shortopt != 0 && !instr(*param, usage[idx].shortopt)) ++idx; if (param[1] == 0) // if the potential argument is separate optarg = (have_more_args ? args[1] : 0); else // if the potential argument is attached optarg = param + 1; } const Descriptor* descriptor = &usage[idx]; if (descriptor->shortopt == 0) /************** unknown option ********************/ { // look for dummy entry (shortopt == "" and longopt == "") to use as Descriptor for unknown options idx = 0; while (usage[idx].shortopt != 0 && (usage[idx].shortopt[0] != 0 || usage[idx].longopt[0] != 0)) ++idx; descriptor = (usage[idx].shortopt == 0 ? 0 : &usage[idx]); } if (descriptor != 0) { Option option(descriptor, param, optarg); switch (descriptor->check_arg(option, print_errors)) { case ARG_ILLEGAL: return false; // fatal case ARG_OK: // skip one element of the argument vector, if it's a separated argument if (optarg != 0 && have_more_args && optarg == args[1]) { shift(args, nonops); if (numargs > 0) --numargs; ++args; } // No further short options are possible after an argument handle_short_options = false; break; case ARG_IGNORE: case ARG_NONE: option.arg = 0; break; } if (!action.perform(option)) return false; } } while (handle_short_options); shift(args, nonops); ++args; if (numargs > 0) --numargs; } // while if (numargs > 0 && *args == 0) // It's a bug in the caller if numargs is greater than the actual number numargs = 0; // of arguments, but as a service to the user we fix this if we spot it. if (numargs < 0) // if we don't know the number of remaining non-option arguments { // we need to count them numargs = 0; while (args[numargs] != 0) ++numargs; } return action.finished(numargs + nonops, args - nonops); } /** * @internal * @brief The implementation of option::printUsage(). */ struct PrintUsageImplementation { /** * @internal * @brief Interface for Functors that write (part of) a string somewhere. */ struct IStringWriter { /** * @brief Writes the given number of chars beginning at the given pointer somewhere. */ virtual void operator()(const char*, int) { } }; /** * @internal * @brief Encapsulates a function with signature func(string, size) where * string can be initialized with a const char* and size with an int. */ template struct FunctionWriter: public IStringWriter { Function* write; virtual void operator()(const char* str, int size) { (*write)(str, size); } FunctionWriter(Function* w) : write(w) { } }; /** * @internal * @brief Encapsulates a reference to an object with a write(string, size) * method like that of @c std::ostream. */ template struct OStreamWriter: public IStringWriter { OStream& ostream; virtual void operator()(const char* str, int size) { ostream.write(str, size); } OStreamWriter(OStream& o) : ostream(o) { } }; /** * @internal * @brief Like OStreamWriter but encapsulates a @c const reference, which is * typically a temporary object of a user class. */ template struct TemporaryWriter: public IStringWriter { const Temporary& userstream; virtual void operator()(const char* str, int size) { userstream.write(str, size); } TemporaryWriter(const Temporary& u) : userstream(u) { } }; /** * @internal * @brief Encapsulates a function with the signature func(fd, string, size) (the * signature of the @c write() system call) * where fd can be initialized from an int, string from a const char* and size from an int. */ template struct SyscallWriter: public IStringWriter { Syscall* write; int fd; virtual void operator()(const char* str, int size) { (*write)(fd, str, size); } SyscallWriter(Syscall* w, int f) : write(w), fd(f) { } }; /** * @internal * @brief Encapsulates a function with the same signature as @c std::fwrite(). */ template struct StreamWriter: public IStringWriter { Function* fwrite; Stream* stream; virtual void operator()(const char* str, int size) { (*fwrite)(str, size, 1, stream); } StreamWriter(Function* w, Stream* s) : fwrite(w), stream(s) { } }; /** * @internal * @brief Sets i1 = max(i1, i2) */ static void upmax(int& i1, int i2) { i1 = (i1 >= i2 ? i1 : i2); } /** * @internal * @brief Moves the "cursor" to column @c want_x assuming it is currently at column @c x * and sets @c x=want_x . * If x > want_x , a line break is output before indenting. * * @param write Spaces and possibly a line break are written via this functor to get * the desired indentation @c want_x . * @param[in,out] x the current indentation. Set to @c want_x by this method. * @param want_x the desired indentation. */ static void indent(IStringWriter& write, int& x, int want_x) { int indent = want_x - x; if (indent < 0) { write("\n", 1); indent = want_x; } if (indent > 0) { char space = ' '; for (int i = 0; i < indent; ++i) write(&space, 1); x = want_x; } } /** * @brief Returns true if ch is the unicode code point of a wide character. * * @note * The following character ranges are treated as wide * @code * 1100..115F * 2329..232A (just 2 characters!) * 2E80..A4C6 except for 303F * A960..A97C * AC00..D7FB * F900..FAFF * FE10..FE6B * FF01..FF60 * FFE0..FFE6 * 1B000...... * @endcode */ static bool isWideChar(unsigned ch) { if (ch == 0x303F) return false; return ((0x1100 <= ch && ch <= 0x115F) || (0x2329 <= ch && ch <= 0x232A) || (0x2E80 <= ch && ch <= 0xA4C6) || (0xA960 <= ch && ch <= 0xA97C) || (0xAC00 <= ch && ch <= 0xD7FB) || (0xF900 <= ch && ch <= 0xFAFF) || (0xFE10 <= ch && ch <= 0xFE6B) || (0xFF01 <= ch && ch <= 0xFF60) || (0xFFE0 <= ch && ch <= 0xFFE6) || (0x1B000 <= ch)); } /** * @internal * @brief Splits a @c Descriptor[] array into tables, rows, lines and columns and * iterates over these components. * * The top-level organizational unit is the @e table. * A table begins at a Descriptor with @c help!=NULL and extends up to * a Descriptor with @c help==NULL. * * A table consists of @e rows. Due to line-wrapping and explicit breaks * a row may take multiple lines on screen. Rows within the table are separated * by \\n. They never cross Descriptor boundaries. This means a row ends either * at \\n or the 0 at the end of the help string. * * A row consists of columns/cells. Columns/cells within a row are separated by \\t. * Line breaks within a cell are marked by \\v. * * Rows in the same table need not have the same number of columns/cells. The * extreme case are interjections, which are rows that contain neither \\t nor \\v. * These are NOT treated specially by LinePartIterator, but they are treated * specially by printUsage(). * * LinePartIterator iterates through the usage at 3 levels: table, row and part. * Tables and rows are as described above. A @e part is a line within a cell. * LinePartIterator iterates through 1st parts of all cells, then through the 2nd * parts of all cells (if any),... @n * Example: The row "1 \v 3 \t 2 \v 4" has 2 cells/columns and 4 parts. * The parts will be returned in the order 1, 2, 3, 4. * * It is possible that some cells have fewer parts than others. In this case * LinePartIterator will "fill up" these cells with 0-length parts. IOW, LinePartIterator * always returns the same number of parts for each column. Note that this is different * from the way rows and columns are handled. LinePartIterator does @e not guarantee that * the same number of columns will be returned for each row. * */ class LinePartIterator { const Descriptor* tablestart; //!< The 1st descriptor of the current table. const Descriptor* rowdesc; //!< The Descriptor that contains the current row. const char* rowstart; //!< Ptr to 1st character of current row within rowdesc->help. const char* ptr; //!< Ptr to current part within the current row. int col; //!< Index of current column. int len; //!< Length of the current part (that ptr points at) in BYTES int screenlen; //!< Length of the current part in screen columns (taking narrow/wide chars into account). int max_line_in_block; //!< Greatest index of a line within the block. This is the number of \\v within the cell with the most \\vs. int line_in_block; //!< Line index within the current cell of the current part. int target_line_in_block; //!< Line index of the parts we should return to the user on this iteration. bool hit_target_line; //!< Flag whether we encountered a part with line index target_line_in_block in the current cell. /** * @brief Determines the byte and character lengths of the part at @ref ptr and * stores them in @ref len and @ref screenlen respectively. */ void update_length() { screenlen = 0; for (len = 0; ptr[len] != 0 && ptr[len] != '\v' && ptr[len] != '\t' && ptr[len] != '\n'; ++len) { ++screenlen; unsigned ch = (unsigned char) ptr[len]; if (ch > 0xC1) // everything <= 0xC1 (yes, even 0xC1 itself) is not a valid UTF-8 start byte { // int __builtin_clz (unsigned int x) // Returns the number of leading 0-bits in x, starting at the most significant bit unsigned mask = (unsigned) -1 >> __builtin_clz(ch ^ 0xff); ch = ch & mask; // mask out length bits, we don't verify their correctness while (((unsigned char) ptr[len + 1] ^ 0x80) <= 0x3F) // while next byte is continuation byte { ch = (ch << 6) ^ (unsigned char) ptr[len + 1] ^ 0x80; // add continuation to char code ++len; } // ch is the decoded unicode code point if (ch >= 0x1100 && isWideChar(ch)) // the test for 0x1100 is here to avoid the function call in the Latin case ++screenlen; } } } public: //! @brief Creates an iterator for @c usage. LinePartIterator(const Descriptor usage[]) : tablestart(usage), rowdesc(0), rowstart(0), ptr(0), col(-1), len(0), max_line_in_block(0), line_in_block(0), target_line_in_block(0), hit_target_line(true) { } /** * @brief Moves iteration to the next table (if any). Has to be called once on a new * LinePartIterator to move to the 1st table. * @retval false if moving to next table failed because no further table exists. */ bool nextTable() { // If this is NOT the first time nextTable() is called after the constructor, // then skip to the next table break (i.e. a Descriptor with help == 0) if (rowdesc != 0) { while (tablestart->help != 0 && tablestart->shortopt != 0) ++tablestart; } // Find the next table after the break (if any) while (tablestart->help == 0 && tablestart->shortopt != 0) ++tablestart; restartTable(); return rowstart != 0; } /** * @brief Reset iteration to the beginning of the current table. */ void restartTable() { rowdesc = tablestart; rowstart = tablestart->help; ptr = 0; } /** * @brief Moves iteration to the next row (if any). Has to be called once after each call to * @ref nextTable() to move to the 1st row of the table. * @retval false if moving to next row failed because no further row exists. */ bool nextRow() { if (ptr == 0) { restartRow(); return rowstart != 0; } while (*ptr != 0 && *ptr != '\n') ++ptr; if (*ptr == 0) { if ((rowdesc + 1)->help == 0) // table break return false; ++rowdesc; rowstart = rowdesc->help; } else // if (*ptr == '\n') { rowstart = ptr + 1; } restartRow(); return true; } /** * @brief Reset iteration to the beginning of the current row. */ void restartRow() { ptr = rowstart; col = -1; len = 0; screenlen = 0; max_line_in_block = 0; line_in_block = 0; target_line_in_block = 0; hit_target_line = true; } /** * @brief Moves iteration to the next part (if any). Has to be called once after each call to * @ref nextRow() to move to the 1st part of the row. * @retval false if moving to next part failed because no further part exists. * * See @ref LinePartIterator for details about the iteration. */ bool next() { if (ptr == 0) return false; if (col == -1) { col = 0; update_length(); return true; } ptr += len; while (true) { switch (*ptr) { case '\v': upmax(max_line_in_block, ++line_in_block); ++ptr; break; case '\t': if (!hit_target_line) // if previous column did not have the targetline { // then "insert" a 0-length part update_length(); hit_target_line = true; return true; } hit_target_line = false; line_in_block = 0; ++col; ++ptr; break; case 0: case '\n': if (!hit_target_line) // if previous column did not have the targetline { // then "insert" a 0-length part update_length(); hit_target_line = true; return true; } if (++target_line_in_block > max_line_in_block) { update_length(); return false; } hit_target_line = false; line_in_block = 0; col = 0; ptr = rowstart; continue; default: ++ptr; continue; } // switch if (line_in_block == target_line_in_block) { update_length(); hit_target_line = true; return true; } } // while } /** * @brief Returns the index (counting from 0) of the column in which * the part pointed to by @ref data() is located. */ int column() { return col; } /** * @brief Returns the index (counting from 0) of the line within the current column * this part belongs to. */ int line() { return target_line_in_block; // NOT line_in_block !!! It would be wrong if !hit_target_line } /** * @brief Returns the length of the part pointed to by @ref data() in raw chars (not UTF-8 characters). */ int length() { return len; } /** * @brief Returns the width in screen columns of the part pointed to by @ref data(). * Takes multi-byte UTF-8 sequences and wide characters into account. */ int screenLength() { return screenlen; } /** * @brief Returns the current part of the iteration. */ const char* data() { return ptr; } }; /** * @internal * @brief Takes input and line wraps it, writing out one line at a time so that * it can be interleaved with output from other columns. * * The LineWrapper is used to handle the last column of each table as well as interjections. * The LineWrapper is called once for each line of output. If the data given to it fits * into the designated width of the last column it is simply written out. If there * is too much data, an appropriate split point is located and only the data up to this * split point is written out. The rest of the data is queued for the next line. * That way the last column can be line wrapped and interleaved with data from * other columns. The following example makes this clearer: * @code * Column 1,1 Column 2,1 This is a long text * Column 1,2 Column 2,2 that does not fit into * a single line. * @endcode * * The difficulty in producing this output is that the whole string * "This is a long text that does not fit into a single line" is the * 1st and only part of column 3. In order to produce the above * output the string must be output piecemeal, interleaved with * the data from the other columns. */ class LineWrapper { static const int bufmask = 15; //!< Must be a power of 2 minus 1. /** * @brief Ring buffer for length component of pair (data, length). */ int lenbuf[bufmask + 1]; /** * @brief Ring buffer for data component of pair (data, length). */ const char* datbuf[bufmask + 1]; /** * @brief The indentation of the column to which the LineBuffer outputs. LineBuffer * assumes that the indentation has already been written when @ref process() * is called, so this value is only used when a buffer flush requires writing * additional lines of output. */ int x; /** * @brief The width of the column to line wrap. */ int width; int head; //!< @brief index for next write int tail; //!< @brief index for next read - 1 (i.e. increment tail BEFORE read) /** * @brief Multiple methods of LineWrapper may decide to flush part of the buffer to * free up space. The contract of process() says that only 1 line is output. So * this variable is used to track whether something has output a line. It is * reset at the beginning of process() and checked at the end to decide if * output has already occurred or is still needed. */ bool wrote_something; bool buf_empty() { return ((tail + 1) & bufmask) == head; } bool buf_full() { return tail == head; } void buf_store(const char* data, int len) { lenbuf[head] = len; datbuf[head] = data; head = (head + 1) & bufmask; } //! @brief Call BEFORE reading ...buf[tail]. void buf_next() { tail = (tail + 1) & bufmask; } /** * @brief Writes (data,len) into the ring buffer. If the buffer is full, a single line * is flushed out of the buffer into @c write. */ void output(IStringWriter& write, const char* data, int len) { if (buf_full()) write_one_line(write); buf_store(data, len); } /** * @brief Writes a single line of output from the buffer to @c write. */ void write_one_line(IStringWriter& write) { if (wrote_something) // if we already wrote something, we need to start a new line { write("\n", 1); int _ = 0; indent(write, _, x); } if (!buf_empty()) { buf_next(); write(datbuf[tail], lenbuf[tail]); } wrote_something = true; } public: /** * @brief Writes out all remaining data from the LineWrapper using @c write. * Unlike @ref process() this method indents all lines including the first and * will output a \\n at the end (but only if something has been written). */ void flush(IStringWriter& write) { if (buf_empty()) return; int _ = 0; indent(write, _, x); wrote_something = false; while (!buf_empty()) write_one_line(write); write("\n", 1); } /** * @brief Process, wrap and output the next piece of data. * * process() will output at least one line of output. This is not necessarily * the @c data passed in. It may be data queued from a prior call to process(). * If the internal buffer is full, more than 1 line will be output. * * process() assumes that the a proper amount of indentation has already been * output. It won't write any further indentation before the 1st line. If * more than 1 line is written due to buffer constraints, the lines following * the first will be indented by this method, though. * * No \\n is written by this method after the last line that is written. * * @param write where to write the data. * @param data the new chunk of data to write. * @param len the length of the chunk of data to write. */ void process(IStringWriter& write, const char* data, int len) { wrote_something = false; while (len > 0) { if (len <= width) // quick test that works because utf8width <= len (all wide chars have at least 2 bytes) { output(write, data, len); len = 0; } else // if (len > width) it's possible (but not guaranteed) that utf8len > width { int utf8width = 0; int maxi = 0; while (maxi < len && utf8width < width) { int charbytes = 1; unsigned ch = (unsigned char) data[maxi]; if (ch > 0xC1) // everything <= 0xC1 (yes, even 0xC1 itself) is not a valid UTF-8 start byte { // int __builtin_clz (unsigned int x) // Returns the number of leading 0-bits in x, starting at the most significant bit unsigned mask = (unsigned) -1 >> __builtin_clz(ch ^ 0xff); ch = ch & mask; // mask out length bits, we don't verify their correctness while ((maxi + charbytes < len) && // (((unsigned char) data[maxi + charbytes] ^ 0x80) <= 0x3F)) // while next byte is continuation byte { ch = (ch << 6) ^ (unsigned char) data[maxi + charbytes] ^ 0x80; // add continuation to char code ++charbytes; } // ch is the decoded unicode code point if (ch >= 0x1100 && isWideChar(ch)) // the test for 0x1100 is here to avoid the function call in the Latin case { if (utf8width + 2 > width) break; ++utf8width; } } ++utf8width; maxi += charbytes; } // data[maxi-1] is the last byte of the UTF-8 sequence of the last character that fits // onto the 1st line. If maxi == len, all characters fit on the line. if (maxi == len) { output(write, data, len); len = 0; } else // if (maxi < len) at least 1 character (data[maxi] that is) doesn't fit on the line { int i; for (i = maxi; i >= 0; --i) if (data[i] == ' ') break; if (i >= 0) { output(write, data, i); data += i + 1; len -= i + 1; } else // did not find a space to split at => split before data[maxi] { // data[maxi] is always the beginning of a character, never a continuation byte output(write, data, maxi); data += maxi; len -= maxi; } } } } if (!wrote_something) // if we didn't already write something to make space in the buffer write_one_line(write); // write at most one line of actual output } /** * @brief Constructs a LineWrapper that wraps its output to fit into * screen columns @c x1 (incl.) to @c x2 (excl.). * * @c x1 gives the indentation LineWrapper uses if it needs to indent. */ LineWrapper(int x1, int x2) : x(x1), width(x2 - x1), head(0), tail(bufmask) { if (width < 2) // because of wide characters we need at least width 2 or the code breaks width = 2; } }; /** * @internal * @brief This is the implementation that is shared between all printUsage() templates. * Because all printUsage() templates share this implementation, there is no template bloat. */ static void printUsage(IStringWriter& write, const Descriptor usage[], int width = 80, // int last_column_min_percent = 50, int last_column_own_line_max_percent = 75) { if (width < 1) // protect against nonsense values width = 80; if (width > 10000) // protect against overflow in the following computation width = 10000; int last_column_min_width = ((width * last_column_min_percent) + 50) / 100; int last_column_own_line_max_width = ((width * last_column_own_line_max_percent) + 50) / 100; if (last_column_own_line_max_width == 0) last_column_own_line_max_width = 1; LinePartIterator part(usage); while (part.nextTable()) { /***************** Determine column widths *******************************/ const int maxcolumns = 8; // 8 columns are enough for everyone int col_width[maxcolumns]; int lastcolumn; int leftwidth; int overlong_column_threshold = 10000; do { lastcolumn = 0; for (int i = 0; i < maxcolumns; ++i) col_width[i] = 0; part.restartTable(); while (part.nextRow()) { while (part.next()) { if (part.column() < maxcolumns) { upmax(lastcolumn, part.column()); if (part.screenLength() < overlong_column_threshold) // We don't let rows that don't use table separators (\t or \v) influence // the width of column 0. This allows the user to interject section headers // or explanatory paragraphs that do not participate in the table layout. if (part.column() > 0 || part.line() > 0 || part.data()[part.length()] == '\t' || part.data()[part.length()] == '\v') upmax(col_width[part.column()], part.screenLength()); } } } /* * If the last column doesn't fit on the same * line as the other columns, we can fix that by starting it on its own line. * However we can't do this for any of the columns 0..lastcolumn-1. * If their sum exceeds the maximum width we try to fix this by iteratively * ignoring the widest line parts in the width determination until * we arrive at a series of column widths that fit into one line. * The result is a layout where everything is nicely formatted * except for a few overlong fragments. * */ leftwidth = 0; overlong_column_threshold = 0; for (int i = 0; i < lastcolumn; ++i) { leftwidth += col_width[i]; upmax(overlong_column_threshold, col_width[i]); } } while (leftwidth > width); /**************** Determine tab stops and last column handling **********************/ int tabstop[maxcolumns]; tabstop[0] = 0; for (int i = 1; i < maxcolumns; ++i) tabstop[i] = tabstop[i - 1] + col_width[i - 1]; int rightwidth = width - tabstop[lastcolumn]; bool print_last_column_on_own_line = false; if (rightwidth < last_column_min_width && // if we don't have the minimum requested width for the last column ( col_width[lastcolumn] == 0 || // and all last columns are > overlong_column_threshold rightwidth < col_width[lastcolumn] // or there is at least one last column that requires more than the space available ) ) { print_last_column_on_own_line = true; rightwidth = last_column_own_line_max_width; } // If lastcolumn == 0 we must disable print_last_column_on_own_line because // otherwise 2 copies of the last (and only) column would be output. // Actually this is just defensive programming. It is currently not // possible that lastcolumn==0 and print_last_column_on_own_line==true // at the same time, because lastcolumn==0 => tabstop[lastcolumn] == 0 => // rightwidth==width => rightwidth>=last_column_min_width (unless someone passes // a bullshit value >100 for last_column_min_percent) => the above if condition // is false => print_last_column_on_own_line==false if (lastcolumn == 0) print_last_column_on_own_line = false; LineWrapper lastColumnLineWrapper(width - rightwidth, width); LineWrapper interjectionLineWrapper(0, width); part.restartTable(); /***************** Print out all rows of the table *************************************/ while (part.nextRow()) { int x = -1; while (part.next()) { if (part.column() > lastcolumn) continue; // drop excess columns (can happen if lastcolumn == maxcolumns-1) if (part.column() == 0) { if (x >= 0) write("\n", 1); x = 0; } indent(write, x, tabstop[part.column()]); if ((part.column() < lastcolumn) && (part.column() > 0 || part.line() > 0 || part.data()[part.length()] == '\t' || part.data()[part.length()] == '\v')) { write(part.data(), part.length()); x += part.screenLength(); } else // either part.column() == lastcolumn or we are in the special case of // an interjection that doesn't contain \v or \t { // NOTE: This code block is not necessarily executed for // each line, because some rows may have fewer columns. LineWrapper& lineWrapper = (part.column() == 0) ? interjectionLineWrapper : lastColumnLineWrapper; if (!print_last_column_on_own_line || part.column() != lastcolumn) lineWrapper.process(write, part.data(), part.length()); } } // while if (print_last_column_on_own_line) { part.restartRow(); while (part.next()) { if (part.column() == lastcolumn) { write("\n", 1); int _ = 0; indent(write, _, width - rightwidth); lastColumnLineWrapper.process(write, part.data(), part.length()); } } } write("\n", 1); lastColumnLineWrapper.flush(write); interjectionLineWrapper.flush(write); } } } } ; /** * @brief Outputs a nicely formatted usage string with support for multi-column formatting * and line-wrapping. * * printUsage() takes the @c help texts of a Descriptor[] array and formats them into * a usage message, wrapping lines to achieve the desired output width. * * Table formatting: * * Aside from plain strings which are simply line-wrapped, the usage may contain tables. Tables * are used to align elements in the output. * * @code * // Without a table. The explanatory texts are not aligned. * -c, --create |Creates something. * -k, --kill |Destroys something. * * // With table formatting. The explanatory texts are aligned. * -c, --create |Creates something. * -k, --kill |Destroys something. * @endcode * * Table formatting removes the need to pad help texts manually with spaces to achieve * alignment. To create a table, simply insert \\t (tab) characters to separate the cells * within a row. * * @code * const option::Descriptor usage[] = { * {..., "-c, --create \tCreates something." }, * {..., "-k, --kill \tDestroys something." }, ... * @endcode * * Note that you must include the minimum amount of space desired between cells yourself. * Table formatting will insert further spaces as needed to achieve alignment. * * You can insert line breaks within cells by using \\v (vertical tab). * * @code * const option::Descriptor usage[] = { * {..., "-c,\v--create \tCreates\vsomething." }, * {..., "-k,\v--kill \tDestroys\vsomething." }, ... * * // results in * * -c, Creates * --create something. * -k, Destroys * --kill something. * @endcode * * You can mix lines that do not use \\t or \\v with those that do. The plain * lines will not mess up the table layout. Alignment of the table columns will * be maintained even across these interjections. * * @code * const option::Descriptor usage[] = { * {..., "-c, --create \tCreates something." }, * {..., "----------------------------------" }, * {..., "-k, --kill \tDestroys something." }, ... * * // results in * * -c, --create Creates something. * ---------------------------------- * -k, --kill Destroys something. * @endcode * * You can have multiple tables within the same usage whose columns are * aligned independently. Simply insert a dummy Descriptor with @c help==0. * * @code * const option::Descriptor usage[] = { * {..., "Long options:" }, * {..., "--very-long-option \tDoes something long." }, * {..., "--ultra-super-mega-long-option \tTakes forever to complete." }, * {..., 0 }, // ---------- table break ----------- * {..., "Short options:" }, * {..., "-s \tShort." }, * {..., "-q \tQuick." }, ... * * // results in * * Long options: * --very-long-option Does something long. * --ultra-super-mega-long-option Takes forever to complete. * Short options: * -s Short. * -q Quick. * * // Without the table break it would be * * Long options: * --very-long-option Does something long. * --ultra-super-mega-long-option Takes forever to complete. * Short options: * -s Short. * -q Quick. * @endcode * * Output methods: * * Because TheLeanMeanC++Option parser is freestanding, you have to provide the means for * output in the first argument(s) to printUsage(). Because printUsage() is implemented as * a set of template functions, you have great flexibility in your choice of output * method. The following example demonstrates typical uses. Anything that's similar enough * will work. * * @code * #include // write() * #include // cout * #include // ostringstream * #include // fwrite() * using namespace std; * * void my_write(const char* str, int size) { * fwrite(str, size, 1, stdout); * } * * struct MyWriter { * void write(const char* buf, size_t size) const { * fwrite(str, size, 1, stdout); * } * }; * * struct MyWriteFunctor { * void operator()(const char* buf, size_t size) { * fwrite(str, size, 1, stdout); * } * }; * ... * printUsage(my_write, usage); // custom write function * printUsage(MyWriter(), usage); // temporary of a custom class * MyWriter writer; * printUsage(writer, usage); // custom class object * MyWriteFunctor wfunctor; * printUsage(&wfunctor, usage); // custom functor * printUsage(write, 1, usage); // write() to file descriptor 1 * printUsage(cout, usage); // an ostream& * printUsage(fwrite, stdout, usage); // fwrite() to stdout * ostringstream sstr; * printUsage(sstr, usage); // an ostringstream& * * @endcode * * @par Notes: * @li the @c write() method of a class that is to be passed as a temporary * as @c MyWriter() is in the example, must be a @c const method, because * temporary objects are passed as const reference. This only applies to * temporary objects that are created and destroyed in the same statement. * If you create an object like @c writer in the example, this restriction * does not apply. * @li a functor like @c MyWriteFunctor in the example must be passed as a pointer. * This differs from the way functors are passed to e.g. the STL algorithms. * @li All printUsage() templates are tiny wrappers around a shared non-template implementation. * So there's no penalty for using different versions in the same program. * @li printUsage() always interprets Descriptor::help as UTF-8 and always produces UTF-8-encoded * output. If your system uses a different charset, you must do your own conversion. You * may also need to change the font of the console to see non-ASCII characters properly. * This is particularly true for Windows. * @li @b Security @b warning: Do not insert untrusted strings (such as user-supplied arguments) * into the usage. printUsage() has no protection against malicious UTF-8 sequences. * * @param prn The output method to use. See the examples above. * @param usage the Descriptor[] array whose @c help texts will be formatted. * @param width the maximum number of characters per output line. Note that this number is * in actual characters, not bytes. printUsage() supports UTF-8 in @c help and will * count multi-byte UTF-8 sequences properly. Asian wide characters are counted * as 2 characters. * @param last_column_min_percent (0-100) The minimum percentage of @c width that should be available * for the last column (which typically contains the textual explanation of an option). * If less space is available, the last column will be printed on its own line, indented * according to @c last_column_own_line_max_percent. * @param last_column_own_line_max_percent (0-100) If the last column is printed on its own line due to * less than @c last_column_min_percent of the width being available, then only * @c last_column_own_line_max_percent of the extra line(s) will be used for the * last column's text. This ensures an indentation. See example below. * * @code * // width=20, last_column_min_percent=50 (i.e. last col. min. width=10) * --3456789 1234567890 * 1234567890 * * // width=20, last_column_min_percent=75 (i.e. last col. min. width=15) * // last_column_own_line_max_percent=75 * --3456789 * 123456789012345 * 67890 * * // width=20, last_column_min_percent=75 (i.e. last col. min. width=15) * // last_column_own_line_max_percent=33 (i.e. max. 5) * --3456789 * 12345 * 67890 * 12345 * 67890 * @endcode */ template void printUsage(OStream& prn, const Descriptor usage[], int width = 80, int last_column_min_percent = 50, int last_column_own_line_max_percent = 75) { PrintUsageImplementation::OStreamWriter write(prn); PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent); } template void printUsage(Function* prn, const Descriptor usage[], int width = 80, int last_column_min_percent = 50, int last_column_own_line_max_percent = 75) { PrintUsageImplementation::FunctionWriter write(prn); PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent); } template void printUsage(const Temporary& prn, const Descriptor usage[], int width = 80, int last_column_min_percent = 50, int last_column_own_line_max_percent = 75) { PrintUsageImplementation::TemporaryWriter write(prn); PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent); } template void printUsage(Syscall* prn, int fd, const Descriptor usage[], int width = 80, int last_column_min_percent = 50, int last_column_own_line_max_percent = 75) { PrintUsageImplementation::SyscallWriter write(prn, fd); PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent); } template void printUsage(Function* prn, Stream* stream, const Descriptor usage[], int width = 80, int last_column_min_percent = 50, int last_column_own_line_max_percent = 75) { PrintUsageImplementation::StreamWriter write(prn, stream); PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent); } } // namespace option #endif /* OPTIONPARSER_H_ */ ================================================ FILE: src/core/program.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "program.h" #include #include #include #include #ifdef __ANDROID__ #include #include #include #include #include #else #include #define GL_GLEXT_PROTOTYPES 1 #include #include #endif #include "log.h" #include "util.h" #ifndef NDEBUG #define WARNINGS_AS_ERRORS #endif static std::string ExpandMacros(std::vector> macros, const std::string& source) { std::string result = source; for (const auto& macro : macros) { std::string::size_type pos = 0; while ((pos = result.find(macro.first, pos)) != std::string::npos) { result.replace(pos, macro.first.length(), macro.second); // Move past the last replaced position pos += macro.second.length(); } } return result; } static void DumpShaderSource(const std::string& source) { std::stringstream ss(source); std::string line; int i = 1; while (std::getline(ss, line)) { Log::D("%04d: %s\n", i, line.c_str()); i++; } Log::D("\n"); } static bool CompileShader(GLenum type, const std::string& source, GLint* shaderOut, const std::string& debugName) { GLint shader = glCreateShader(type); int size = static_cast(source.size()); const GLchar* sourcePtr = source.c_str(); glShaderSource(shader, 1, (const GLchar**)&sourcePtr, &size); glCompileShader(shader); GLint compiled; glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); if (!compiled) { Log::E("shader compilation error for \"%s\"!\n", debugName.c_str()); } GLint bufferLen = 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &bufferLen); if (bufferLen > 1) { if (compiled) { Log::E("shader compilation warning for \"%s\"!\n", debugName.c_str()); } GLsizei len = 0; std::unique_ptr buffer(new char[bufferLen]); glGetShaderInfoLog(shader, bufferLen, &len, buffer.get()); Log::E("%s\n", buffer.get()); DumpShaderSource(source); } #ifdef WARNINGS_AS_ERRORS if (!compiled || bufferLen > 1) #else if (!compiled) #endif { return false; } *shaderOut = shader; return true; } Program::Program() : program(0), vertShader(0), geomShader(0), fragShader(0), computeShader(0) { #ifdef __ANDROID__ AddMacro("HEADER", "#version 320 es\nprecision highp float;"); #else AddMacro("HEADER", "#version 460"); #endif } Program::~Program() { Delete(); } void Program::AddMacro(const std::string& key, const std::string& value) { // In order to keep the glsl code compiling if the macro is not applied. // the key is enclosed in a c-style comment and double %. std::string token = "/*%%" + key + "%%*/"; macros.push_back(std::pair(token, value)); } bool Program::LoadVertFrag(const std::string& vertFilename, const std::string& fragFilename) { return LoadVertGeomFrag(vertFilename, std::string(), fragFilename); } bool Program::LoadVertGeomFrag(const std::string& vertFilename, const std::string& geomFilename, const std::string& fragFilename) { // Delete old shader/program Delete(); const bool useGeomShader = !geomFilename.empty(); if (useGeomShader) { debugName = vertFilename + " + " + geomFilename + " + " + fragFilename; } else { debugName = vertFilename + " + " + fragFilename; } std::string vertSource, geomSource, fragSource; if (!LoadFile(vertFilename, vertSource)) { Log::E("Failed to load vertex shader %s\n", vertFilename.c_str()); return false; } vertSource = ExpandMacros(macros, vertSource); if (useGeomShader) { if (!LoadFile(geomFilename, geomSource)) { Log::E("Failed to load geometry shader %s\n", geomFilename.c_str()); return false; } geomSource = ExpandMacros(macros, geomSource); } if (!LoadFile(fragFilename, fragSource)) { Log::E("Failed to load fragment shader \"%s\"\n", fragFilename.c_str()); return false; } fragSource = ExpandMacros(macros, fragSource); if (!CompileShader(GL_VERTEX_SHADER, vertSource, &vertShader, vertFilename)) { Log::E("Failed to compile vertex shader \"%s\"\n", vertFilename.c_str()); return false; } if (useGeomShader) { geomSource = ExpandMacros(macros, geomSource); if (!CompileShader(GL_GEOMETRY_SHADER, geomSource, &geomShader, geomFilename)) { Log::E("Failed to compile geometry shader \"%s\"\n", geomFilename.c_str()); return false; } } if (!CompileShader(GL_FRAGMENT_SHADER, fragSource, &fragShader, fragFilename)) { Log::E("Failed to compile fragment shader \"%s\"\n", fragFilename.c_str()); return false; } program = glCreateProgram(); glAttachShader(program, vertShader); glAttachShader(program, fragShader); if (useGeomShader) { glAttachShader(program, geomShader); } glLinkProgram(program); if (!CheckLinkStatus()) { Log::E("Failed to link program \"%s\"\n", debugName.c_str()); // dump shader source for reference Log::D("\n"); Log::D("%s =\n", vertFilename.c_str()); DumpShaderSource(vertSource); if (useGeomShader) { Log::D("%s =\n", geomFilename.c_str()); DumpShaderSource(geomSource); } Log::D("%s =\n", fragFilename.c_str()); DumpShaderSource(fragSource); return false; } const int MAX_NAME_SIZE = 1028; static char name[MAX_NAME_SIZE]; GLint numAttribs; glGetProgramiv(program, GL_ACTIVE_ATTRIBUTES, &numAttribs); for (int i = 0; i < numAttribs; ++i) { Variable v; GLsizei strLen; glGetActiveAttrib(program, i, MAX_NAME_SIZE, &strLen, &v.size, &v.type, name); v.loc = glGetAttribLocation(program, name); attribs[name] = v; } GLint numUniforms; glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &numUniforms); for (int i = 0; i < numUniforms; ++i) { Variable v; GLsizei strLen; glGetActiveUniform(program, i, MAX_NAME_SIZE, &strLen, &v.size, &v.type, name); int loc = glGetUniformLocation(program, name); v.loc = loc; uniforms[name] = v; } return true; } bool Program::LoadCompute(const std::string& computeFilename) { // Delete old shader/program Delete(); debugName = computeFilename; GL_ERROR_CHECK("Program::LoadCompute begin"); std::string computeSource; if (!LoadFile(computeFilename, computeSource)) { Log::E("Failed to load compute shader \"%s\"\n", computeFilename.c_str()); return false; } GL_ERROR_CHECK("Program::LoadCompute LoadFile"); computeSource = ExpandMacros(macros, computeSource); if (!CompileShader(GL_COMPUTE_SHADER, computeSource, &computeShader, computeFilename)) { Log::E("Failed to compile compute shader \"%s\"\n", computeFilename.c_str()); return false; } GL_ERROR_CHECK("Program::LoadCompute CompileShader"); program = glCreateProgram(); glAttachShader(program, computeShader); glLinkProgram(program); GL_ERROR_CHECK("Program::LoadCompute Attach and Link"); if (!CheckLinkStatus()) { Log::E("Failed to link program \"%s\"\n", debugName.c_str()); // dump shader source for reference Log::D("\n"); Log::D("%s =\n", computeFilename.c_str()); DumpShaderSource(computeSource); return false; } const int MAX_NAME_SIZE = 1028; static char name[MAX_NAME_SIZE]; GLint numUniforms; glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &numUniforms); for (int i = 0; i < numUniforms; ++i) { Variable v; GLsizei strLen; glGetActiveUniform(program, i, MAX_NAME_SIZE, &strLen, &v.size, &v.type, name); int loc = glGetUniformLocation(program, name); v.loc = loc; uniforms[name] = v; } GL_ERROR_CHECK("Program::LoadCompute get uniforms"); // TODO: build reflection info on shader storage blocks return true; } void Program::Bind() const { glUseProgram(program); } int Program::GetUniformLoc(const std::string& name) const { auto iter = uniforms.find(name); if (iter != uniforms.end()) { return iter->second.loc; } else { assert(false); Log::W("Could not find uniform \"%s\" for program \"%s\"\n", name.c_str(), debugName.c_str()); return 0; } } int Program::GetAttribLoc(const std::string& name) const { auto iter = attribs.find(name); if (iter != attribs.end()) { return iter->second.loc; } else { Log::W("Could not find attrib \"%s\" for program \"%s\"\n", name.c_str(), debugName.c_str()); assert(false); return 0; } } void Program::SetUniformRaw(int loc, uint32_t value) const { glUniform1ui(loc, value); } void Program::SetUniformRaw(int loc, int32_t value) const { glUniform1i(loc, value); } void Program::SetUniformRaw(int loc, float value) const { glUniform1f(loc, value); } void Program::SetUniformRaw(int loc, const glm::vec2& value) const { glUniform2fv(loc, 1, (float*)&value); } void Program::SetUniformRaw(int loc, const glm::vec3& value) const { glUniform3fv(loc, 1, (float*)&value); } void Program::SetUniformRaw(int loc, const glm::vec4& value) const { glUniform4fv(loc, 1, (float*)&value); } void Program::SetUniformRaw(int loc, const glm::mat2& value) const { glUniformMatrix2fv(loc, 1, GL_FALSE, (float*)&value); } void Program::SetUniformRaw(int loc, const glm::mat3& value) const { glUniformMatrix3fv(loc, 1, GL_FALSE, (float*)&value); } void Program::SetUniformRaw(int loc, const glm::mat4& value) const { glUniformMatrix4fv(loc, 1, GL_FALSE, (float*)&value); } void Program::SetAttribRaw(int loc, float* values, size_t stride) const { glVertexAttribPointer(loc, 1, GL_FLOAT, GL_FALSE, (GLsizei)stride, values); glEnableVertexAttribArray(loc); } void Program::SetAttribRaw(int loc, glm::vec2* values, size_t stride) const { glVertexAttribPointer(loc, 2, GL_FLOAT, GL_FALSE, (GLsizei)stride, values); glEnableVertexAttribArray(loc); } void Program::SetAttribRaw(int loc, glm::vec3* values, size_t stride) const { glVertexAttribPointer(loc, 3, GL_FLOAT, GL_FALSE, (GLsizei)stride, values); glEnableVertexAttribArray(loc); } void Program::SetAttribRaw(int loc, glm::vec4* values, size_t stride) const { glVertexAttribPointer(loc, 4, GL_FLOAT, GL_FALSE, (GLsizei)stride, values); glEnableVertexAttribArray(loc); } void Program::Delete() { debugName = ""; if (vertShader > 0) { glDeleteShader(vertShader); vertShader = 0; } if (geomShader > 0) { glDeleteShader(geomShader); geomShader = 0; } if (fragShader > 0) { glDeleteShader(fragShader); fragShader = 0; } if (computeShader > 0) { glDeleteShader(computeShader); computeShader = 0; } if (program > 0) { glDeleteProgram(program); program = 0; } uniforms.clear(); attribs.clear(); } bool Program::CheckLinkStatus() { GLint linked; glGetProgramiv(program, GL_LINK_STATUS, &linked); if (!linked) { Log::E("Failed to link shaders \"%s\"\n", debugName.c_str()); } const GLint MAX_BUFFER_LEN = 4096; GLsizei bufferLen = 0; std::unique_ptr buffer(new char[MAX_BUFFER_LEN]); glGetProgramInfoLog(program, MAX_BUFFER_LEN, &bufferLen, buffer.get()); if (bufferLen > 0) { if (linked) { Log::W("Warning during linking shaders \"%s\"\n", debugName.c_str()); } Log::W("%s\n", buffer.get()); } #ifdef WARNINGS_AS_ERRORS if (!linked || bufferLen > 1) #else if (!linked) #endif { return false; } return true; } ================================================ FILE: src/core/program.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include "core/log.h" class Program { public: Program(); ~Program(); // used to inject #defines or other code into shaders // AddMacro("FOO", "BAR"); // will replace the string /*%%FOO%%*/ in the source shader with BAR void AddMacro(const std::string& key, const std::string& value); bool LoadVertFrag(const std::string& vertFilename, const std::string& fragFilename); bool LoadVertGeomFrag(const std::string& vertFilename, const std::string& geomFilename, const std::string& fragFilename); bool LoadCompute(const std::string& computeFilename); void Bind() const; int GetUniformLoc(const std::string& name) const; int GetAttribLoc(const std::string& name) const; template void SetUniform(const std::string& name, T value) const { auto iter = uniforms.find(name); if (iter != uniforms.end()) { SetUniformRaw(iter->second.loc, value); } else { Log::W("Could not find uniform \"%s\" for program \"%s\"\n", name.c_str(), debugName.c_str()); } } void SetUniformRaw(int loc, int32_t value) const; void SetUniformRaw(int loc, uint32_t value) const; void SetUniformRaw(int loc, float value) const; void SetUniformRaw(int loc, const glm::vec2& value) const; void SetUniformRaw(int loc, const glm::vec3& value) const; void SetUniformRaw(int loc, const glm::vec4& value) const; void SetUniformRaw(int loc, const glm::mat2& value) const; void SetUniformRaw(int loc, const glm::mat3& value) const; void SetUniformRaw(int loc, const glm::mat4& value) const; template void SetAttrib(const std::string& name, T* values, size_t stride = 0) const { auto iter = attribs.find(name); if (iter != attribs.end()) { SetAttribRaw(iter->second.loc, values, stride); } else { Log::W("Could not find attrib \"%s\" for program \"%s\"\n", name.c_str(), debugName.c_str()); } } void SetAttribRaw(int loc, float* values, size_t stride = 0) const; void SetAttribRaw(int loc, glm::vec2* values, size_t stride = 0) const; void SetAttribRaw(int loc, glm::vec3* values, size_t stride = 0) const; void SetAttribRaw(int loc, glm::vec4* values, size_t stride = 0) const; protected: void Delete(); bool CheckLinkStatus(); int program; int vertShader; int geomShader; int fragShader; int computeShader; struct Variable { int size; uint32_t type; int loc; }; std::unordered_map uniforms; std::unordered_map attribs; std::vector> macros; std::string debugName; }; ================================================ FILE: src/core/statemachine.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include "log.h" template class StateMachine { public: using ProcessCallback = std::function; using VoidCallback = std::function; using BoolCallback = std::function; protected: struct TransitionStruct { TransitionStruct(const BoolCallback& cbIn, State stateIn, std::string nameIn) : cb(cbIn), state(stateIn), name(nameIn) {} BoolCallback cb; State state; std::string name; }; struct StateStruct { StateStruct(const VoidCallback& enterIn, const VoidCallback& exitIn, const ProcessCallback& processIn) : enter(enterIn), exit(exitIn), process(processIn) {} VoidCallback enter; VoidCallback exit; ProcessCallback process; std::vector transitionVec; }; using StatePair = std::pair; public: StateMachine(State defaultState) : state(defaultState), debug(false) { ; } void AddState(State state, const std::string& name, const VoidCallback& enter, const VoidCallback& exit, const ProcessCallback& process) { StatePair sp(state, StateStruct(enter, exit, process)); stateStructMap.insert(sp); stateNameMap.insert(std::pair(state, name)); } void AddTransition(State state, State newState, const std::string& name, const BoolCallback& transitionCb) { stateStructMap.at(state).transitionVec.push_back(TransitionStruct(transitionCb, newState, name)); } void Process(float dt) { for (auto&& trans : stateStructMap.at(state).transitionVec) { if (trans.cb()) { ChangeState(trans.state, trans.name); } } stateStructMap.at(state).process(dt); } void ChangeState(State newState, const std::string& reason) { if (debug) { Log::D("StateChange from %s -> %s, (%s)\n", stateNameMap.at(state).c_str(), stateNameMap.at(newState).c_str(), reason.c_str()); } stateStructMap.at(state).exit(); stateStructMap.at(newState).enter(); state = newState; } void SetDebug(bool debugIn) { debug = debugIn; } protected: State state; std::map stateStructMap; std::map stateNameMap; bool debug; }; ================================================ FILE: src/core/textrenderer.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "textrenderer.h" #include #ifdef __ANDROID__ #include #include #include #include #include #else #include #endif #include #include "core/image.h" #include "core/log.h" #include "core/util.h" #include "core/program.h" #include "core/texture.h" const int TAB_SIZE = 4; static TextRenderer::TextKey nextKey = 1; TextRenderer::TextRenderer() { } bool TextRenderer::Init(const std::string& fontJsonFilename, const std::string& fontPngFilename) { std::ifstream f(GetRootPath() + fontJsonFilename); if (f.fail()) { return false; } try { nlohmann::json j = nlohmann::json::parse(f); textureWidth = j["texture_width"].template get(); nlohmann::json metrics = j["glyph_metrics"]; for (auto& iter : metrics.items()) { int key = iter.value()["ascii_index"].template get(); if (key < 0 && key > std::numeric_limits::max()) { Log::W("TextRenderer(%s) glyph %d is out of range\n", fontJsonFilename.c_str(), key); continue; } Glyph g; nlohmann::json v2 = iter.value()["xy_lower_left"]; g.xyMin = glm::vec2(v2[0].template get(), v2[1].template get()); v2 = iter.value()["xy_upper_right"]; g.xyMax = glm::vec2(v2[0].template get(), v2[1].template get()); v2 = iter.value()["uv_lower_left"]; g.uvMin = glm::vec2(v2[0].template get(), v2[1].template get()); v2 = iter.value()["uv_upper_right"]; g.uvMax = glm::vec2(v2[0].template get(), v2[1].template get()); v2 = iter.value()["advance"]; g.advance = glm::vec2(v2[0].template get(), v2[1].template get()); glyphMap.insert(std::pair((uint8_t)key, g)); } // TODO: support kerning table, for variable width fonts } catch (const nlohmann::json::exception& e) { std::string s = e.what(); Log::E("TextRenderer::Init(%s) exception: %s\n", fontJsonFilename.c_str(), s.c_str()); return false; } // find the spaceGlyph auto gIter = glyphMap.find((uint8_t)' '); assert(gIter != glyphMap.end()); if (gIter != glyphMap.end()) { spaceGlyph = gIter->second; } Image fontImg; if (!fontImg.Load(fontPngFilename)) { Log::E("Error loading fontPng\n"); return false; } // TODO: get gamma correct. //fontImg.isSRGB = isFramebufferSRGBEnabled; Texture::Params texParams = {FilterType::LinearMipmapLinear, FilterType::Linear, WrapType::ClampToEdge, WrapType::ClampToEdge}; fontTex = std::make_shared(fontImg, texParams); textProg = std::make_shared(); if (!textProg->LoadVertFrag("shader/text_vert.glsl", "shader/text_frag.glsl")) { Log::E("Error loading TextRenderer shader!\n"); return false; } return true; } void TextRenderer::Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar) { textProg->Bind(); // use texture unit 0 for fontTexture glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, fontTex->texture); textProg->SetUniform("fontTex", 0); glm::mat4 viewProjMat = projMat * glm::inverse(cameraMat); float aspect = viewport.w / viewport.z; glm::mat4 aspectMat = MakeMat4(glm::vec3(aspect, 1.0f, 1.0f), glm::quat(), glm::vec3(-aspect / aspect, 0.0f, 0.0f)); for (auto&& tIter : textMap) { if (tIter.second.isScreenAligned) { textProg->SetUniform("modelViewProjMat", aspectMat * tIter.second.xform); } else { textProg->SetUniform("modelViewProjMat", viewProjMat * tIter.second.xform); } textProg->SetAttrib("position", tIter.second.posVec.data()); textProg->SetAttrib("uv", tIter.second.uvVec.data()); textProg->SetAttrib("color", tIter.second.colorVec.data()); glDrawArrays(GL_TRIANGLES, 0, (GLsizei)tIter.second.posVec.size()); } } // creates a new text and adds it to the scene TextRenderer::TextKey TextRenderer::AddWorldText(const glm::mat4 xform, const glm::vec4& color, float lineHeight, const std::string& asciiString) { Text text; text.xform = xform; text.posVec.reserve(asciiString.size() * 6); text.uvVec.reserve(asciiString.size() * 6); text.colorVec.reserve(asciiString.size() * 6); text.isScreenAligned = false; glm::vec3 pen(0.0f, 0.0f, 0.0f); BuildText(text, pen, lineHeight, color, asciiString); uint32_t textKey = nextKey++; textMap.insert(std::pair(textKey, text)); return textKey; } TextRenderer::TextKey TextRenderer::AddScreenText(const glm::ivec2& pos, int numRows, const glm::vec4& color, const std::string& asciiString) { const bool addDropShadow = false; return AddScreenTextImpl(pos, numRows, color, asciiString, addDropShadow, glm::vec4()); } TextRenderer::TextKey TextRenderer::AddScreenTextWithDropShadow(const glm::ivec2& pos, int numRows, const glm::vec4& color, const glm::vec4& shadowColor, const std::string& asciiString) { const bool addDropShadow = true; return AddScreenTextImpl(pos, numRows, color, asciiString, addDropShadow, shadowColor); } void TextRenderer::SetTextXform(TextKey key, const glm::mat4 xform) { auto tIter = textMap.find(key); if (tIter != textMap.end()) { tIter->second.xform = xform; } } // removes text object form the scene void TextRenderer::RemoveText(TextKey key) { textMap.erase(key); } void TextRenderer::BuildText(Text& text, const glm::vec3& pen, float lineHeight, const glm::vec4& color, const std::string& asciiString) const { int r = 0; int c = 0; glm::vec2 penxy = pen; float depth = pen.z; for (auto&& ch : asciiString) { if (ch == ' ') { penxy += lineHeight * spaceGlyph.advance; c++; } else if (ch == '\n') { penxy = lineHeight * glm::vec2(0.0f, (float)-(r + 1)); r++; } else if (ch == '\t') { int numSpaces = TAB_SIZE - (c % TAB_SIZE); penxy += lineHeight * (float)numSpaces * spaceGlyph.advance; c += numSpaces; } else { auto gIter = glyphMap.find((uint8_t)ch); if (gIter == glyphMap.end()) { continue; } const Glyph& g = gIter->second; text.posVec.push_back(glm::vec3(penxy + lineHeight * g.xyMin, depth)); text.posVec.push_back(glm::vec3(penxy + lineHeight * g.xyMax, depth)); text.posVec.push_back(glm::vec3(penxy + lineHeight * glm::vec2(g.xyMin.x, g.xyMax.y), depth)); text.posVec.push_back(glm::vec3(penxy + lineHeight * g.xyMin, depth)); text.posVec.push_back(glm::vec3(penxy + lineHeight * glm::vec2(g.xyMax.x, g.xyMin.y), depth)); text.posVec.push_back(glm::vec3(penxy + lineHeight * g.xyMax, depth)); text.uvVec.push_back(g.uvMin); text.uvVec.push_back(g.uvMax); text.uvVec.push_back(glm::vec2(g.uvMin.x, g.uvMax.y)); text.uvVec.push_back(g.uvMin); text.uvVec.push_back(glm::vec2(g.uvMax.x, g.uvMin.y)); text.uvVec.push_back(g.uvMax); for (int i = 0; i < 6; i++) { text.colorVec.push_back(color); } penxy += lineHeight * g.advance; c++; } } } TextRenderer::TextKey TextRenderer::AddScreenTextImpl(const glm::ivec2& pos, int numRows, const glm::vec4& color, const std::string& asciiString, bool addDropShadow, const glm::vec4& shadowColor) { const float TEXT_LINE_HEIGHT = 2.0f / numRows; glm::vec3 origin(0.1f * TEXT_LINE_HEIGHT, 1.0f - 0.75f * TEXT_LINE_HEIGHT, 0.0f); glm::vec3 offset((float)pos.x * spaceGlyph.advance.x * TEXT_LINE_HEIGHT, (float)pos.y * -TEXT_LINE_HEIGHT, 0.0f); Text text; text.xform = MakeMat4(glm::quat(), origin + offset); size_t vecSize = addDropShadow ? (asciiString.size() * 6 * 2) : (asciiString.size() * 6); text.posVec.reserve(vecSize); text.uvVec.reserve(vecSize); text.colorVec.reserve(vecSize); text.isScreenAligned = true; if (addDropShadow) { glm::vec3 shadowPen = glm::vec3(0.05f * TEXT_LINE_HEIGHT, -0.05f * TEXT_LINE_HEIGHT, 0.1f); BuildText(text, shadowPen, TEXT_LINE_HEIGHT, shadowColor, asciiString); } glm::vec3 pen(0.0f, 0.0f, 0.0f); BuildText(text, pen, TEXT_LINE_HEIGHT, color, asciiString); uint32_t textKey = nextKey++; textMap.insert(std::pair(textKey, text)); return textKey; } ================================================ FILE: src/core/textrenderer.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include #include class Program; struct Texture; class TextRenderer { public: TextRenderer(); bool Init(const std::string& fontJsonFilename, const std::string& fontPngFilename); // viewport = (x, y, width, height) void Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar); using TextKey = uint32_t; // creates a new text and adds it to the scene TextKey AddWorldText(const glm::mat4 xform, const glm::vec4& color, float lineHeight, const std::string& asciiString); TextKey AddScreenText(const glm::ivec2& pos, int numRows, const glm::vec4& color, const std::string& asciiString); TextKey AddScreenTextWithDropShadow(const glm::ivec2& pos, int numRows, const glm::vec4& color, const glm::vec4& shadowColor, const std::string& asciiString); void SetTextXform(TextKey key, const glm::mat4 xform); // removes text object form the scene void RemoveText(TextKey key); protected: struct Glyph { glm::vec2 xyMin; glm::vec2 xyMax; glm::vec2 uvMin; glm::vec2 uvMax; glm::vec2 advance; }; struct Text { glm::mat4 xform; std::vector posVec; std::vector uvVec; std::vector colorVec; bool isScreenAligned; }; void BuildText(Text& text, const glm::vec3& pen, float lineHeight, const glm::vec4& color, const std::string& asciiString) const; TextKey AddScreenTextImpl(const glm::ivec2& pos, int numRows, const glm::vec4& color, const std::string& asciiString, bool addDropShadow, const glm::vec4& shadowColor); std::unordered_map glyphMap; float textureWidth; std::shared_ptr textProg; std::shared_ptr fontTex; std::unordered_map textMap; Glyph spaceGlyph; }; ================================================ FILE: src/core/texture.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "texture.h" #ifdef __ANDROID__ #include #include #include #include #else #include #define GL_GLEXT_PROTOTYPES 1 #include #include #endif #include "core/image.h" static GLenum filterTypeToGL[] = { GL_NEAREST, GL_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR }; static GLenum wrapTypeToGL[] = { GL_REPEAT, GL_MIRRORED_REPEAT, GL_CLAMP_TO_EDGE, #ifndef __ANDROID__ GL_MIRROR_CLAMP_TO_EDGE, #endif }; static GLenum pixelFormatToGL[] = { GL_LUMINANCE, GL_LUMINANCE_ALPHA, GL_RGB, GL_RGBA }; Texture::Texture(const Image& image, const Params& params) { glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterTypeToGL[(int)params.minFilter]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterTypeToGL[(int)params.magFilter]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapTypeToGL[(int)params.sWrap]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapTypeToGL[(int)params.tWrap]); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); GLenum pf = pixelFormatToGL[(int)image.pixelFormat]; int internalFormat = pf; if (image.isSRGB && pf == GL_RGB) { internalFormat = GL_SRGB8; } else if (image.isSRGB && pf == GL_RGBA) { internalFormat = GL_SRGB8_ALPHA8; } glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, image.width, image.height, 0, pf, GL_UNSIGNED_BYTE, &image.data[0]); if ((int)params.minFilter >= (int)FilterType::NearestMipmapNearest) { glGenerateMipmap(GL_TEXTURE_2D); } if (image.pixelFormat == PixelFormat::RA || image.pixelFormat == PixelFormat::RGBA) { hasAlphaChannel = true; } } Texture::Texture(uint32_t width, uint32_t height, uint32_t internalFormat, uint32_t format, uint32_t type, const Params& params) { glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterTypeToGL[(int)params.minFilter]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterTypeToGL[(int)params.magFilter]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapTypeToGL[(int)params.sWrap]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapTypeToGL[(int)params.tWrap]); glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, type, nullptr); } Texture::~Texture() { glDeleteTextures(1, &texture); } void Texture::Bind(int unit) const { glActiveTexture(GL_TEXTURE0 + unit); glBindTexture(GL_TEXTURE_2D, texture); } ================================================ FILE: src/core/texture.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include struct Image; enum class FilterType { Nearest = 0, Linear, NearestMipmapNearest, LinearMipmapNearest, NearestMipmapLinear, LinearMipmapLinear }; enum class WrapType { Repeat = 0, MirroredRepeat, ClampToEdge, MirrorClampToEdge }; struct Texture { struct Params { FilterType minFilter; FilterType magFilter; WrapType sWrap; WrapType tWrap; }; Texture(const Image& image, const Params& params); Texture(uint32_t width, uint32_t height, uint32_t internalFormat, uint32_t format, uint32_t type, const Params& params); ~Texture(); void Bind(int unit) const; uint32_t texture; bool hasAlphaChannel; }; ================================================ FILE: src/core/util.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "util.h" #include #include #include #include #include #include #ifndef __ANDROID__ #include #define GL_GLEXT_PROTOTYPES 1 #include #include #include #else #include #include #include #include #endif #include "log.h" bool LoadFile(const std::string& filename, std::string& data) { std::ifstream ifs(GetRootPath() + filename, std::ifstream::in); if (ifs.good()) { std::string content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); data = std::move(content); return true; } else { return false; } } bool SaveFile(const std::string& filename, const std::string& data) { std::ofstream ofs(GetRootPath() + filename, std::ofstream::out); if (ofs.good()) { ofs << data; return true; } else { return false; } } // returns the number of bytes to advance // fills cp_out with the code point at p. int NextCodePointUTF8(const char *str, uint32_t *codePointOut) { const uint8_t* p = (const uint8_t*)str; if ((*p & 0x80) == 0) { *codePointOut = *p; return 1; } else if ((*p & 0xe0) == 0xc0) // 110xxxxx 10xxxxxx { *codePointOut = ((*p & ~0xe0) << 6) | (*(p+1) & ~0xc0); return 2; } else if ((*p & 0xf0) == 0xe0) // 1110xxxx 10xxxxxx 10xxxxxx { *codePointOut = ((*p & ~0xf0) << 12) | ((*(p+1) & ~0xc0) << 6) | (*(p+2) & ~0xc0); return 3; } else if ((*p & 0xf8) == 0xf0) // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx { *codePointOut = ((*p & ~0xf8) << 18) | ((*(p+1) & ~0xc0) << 12) | ((*(p+1) & ~0xc0) << 6) | (*(p+2) & ~0xc0); return 4; } else { // p is not at a valid starting point. p is not utf8 encoded or is at a bad offset. assert(0); *codePointOut = 0; return 1; } } #ifndef NDEBUG // If there is a glError this outputs it along with a message to stderr. // otherwise there is no output. void GLErrorCheck(const char* message) { GLenum val = glGetError(); switch (val) { case GL_INVALID_ENUM: Log::D("GL_INVALID_ENUM : %s\n", message); break; case GL_INVALID_VALUE: Log::D("GL_INVALID_VALUE : %s\n", message); break; case GL_INVALID_OPERATION: Log::D("GL_INVALID_OPERATION : %s\n", message); break; #ifndef GL_ES_VERSION_2_0 case GL_STACK_OVERFLOW: Log::D("GL_STACK_OVERFLOW : %s\n", message); break; case GL_STACK_UNDERFLOW: Log::D("GL_STACK_UNDERFLOW : %s\n", message); break; #endif case GL_OUT_OF_MEMORY: Log::D("GL_OUT_OF_MEMORY : %s\n", message); break; case GL_NO_ERROR: break; } } #endif glm::vec3 SafeNormalize(const glm::vec3& v, const glm::vec3& ifZero) { float len = glm::length(v); if (len > 0.0f) { return glm::normalize(v); } else { return ifZero; } } glm::quat SafeMix(const glm::quat& a, const glm::quat& b, float alpha) { // adjust signs if necessary glm::quat bTemp = b; float dot = glm::dot(a, bTemp); if (dot < 0.0f) { bTemp = -bTemp; } return glm::normalize(glm::lerp(a, bTemp, alpha)); } glm::mat3 MakeMat3(const glm::quat& rotation) { glm::vec3 xAxis = rotation * glm::vec3(1.0f, 0.0f, 0.0f); glm::vec3 yAxis = rotation * glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 zAxis = rotation * glm::vec3(0.0f, 0.0f, 1.0f); return glm::mat3(xAxis, yAxis, zAxis); } glm::mat3 MakeMat3(const glm::vec3& scale, const glm::quat& rotation) { glm::vec3 xAxis = rotation * glm::vec3(scale.x, 0.0f, 0.0f); glm::vec3 yAxis = rotation * glm::vec3(0.0f, scale.y, 0.0f); glm::vec3 zAxis = rotation * glm::vec3(0.0f, 0.0f, scale.z); return glm::mat3(xAxis, yAxis, zAxis); } glm::mat4 MakeMat3(float scale, const glm::quat& rotation) { return MakeMat3(glm::vec3(scale), rotation); } glm::mat4 MakeMat4(const glm::vec3& scale, const glm::quat& rotation, const glm::vec3& translation) { glm::vec3 xAxis = rotation * glm::vec3(scale.x, 0.0f, 0.0f); glm::vec3 yAxis = rotation * glm::vec3(0.0f, scale.y, 0.0f); glm::vec3 zAxis = rotation * glm::vec3(0.0f, 0.0f, scale.z); return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f), glm::vec4(zAxis, 0.0f), glm::vec4(translation, 1.0f)); } glm::mat4 MakeMat4(float scale, const glm::quat& rotation, const glm::vec3& translation) { return MakeMat4(glm::vec3(scale), rotation, translation); } glm::mat4 MakeMat4(const glm::quat& rotation, const glm::vec3& translation) { return MakeMat4(glm::vec3(1.0f), rotation, translation); } glm::mat4 MakeMat4(const glm::quat& rotation) { return MakeMat4(glm::vec3(1.0f), rotation, glm::vec3(0.0f)); } glm::mat4 MakeMat4(const glm::mat3& m3, const glm::vec3& translation) { return glm::mat4(glm::vec4(m3[0], 0.0f), glm::vec4(m3[1], 0.0f), glm::vec4(m3[2], 0.0f), glm::vec4(translation, 1.0f)); } glm::mat4 MakeMat4(const glm::mat3& m3) { return MakeMat4(m3, glm::vec3(0.0f)); } void Decompose(const glm::mat4& matrix, glm::vec3* scaleOut, glm::quat* rotationOut, glm::vec3* translationOut) { glm::mat3 m(matrix); float det = glm::determinant(m); if (det < 0.0f) { // left handed matrix, flip sign to compensate. *scaleOut = glm::vec3(-glm::length(m[0]), glm::length(m[1]), glm::length(m[2])); } else { *scaleOut = glm::vec3(glm::length(m[0]), glm::length(m[1]), glm::length(m[2])); } // quat_cast doesn't work so well with scaled matrices, so cancel it out. glm::mat4 tmp = glm::scale(matrix, 1.0f / *scaleOut); *rotationOut = glm::normalize(glm::quat_cast(tmp)); *translationOut = glm::vec3(matrix[3][0], matrix[3][1], matrix[3][2]); } void Decompose(const glm::mat3& matrix, glm::vec3* scaleOut, glm::quat* rotationOut) { float det = glm::determinant(matrix); if (det < 0.0f) { // left handed matrix, flip sign to compensate. *scaleOut = glm::vec3(-glm::length(matrix[0]), glm::length(matrix[1]), glm::length(matrix[2])); } else { *scaleOut = glm::vec3(glm::length(matrix[0]), glm::length(matrix[1]), glm::length(matrix[2])); } // quat_cast doesn't work so well with scaled matrices, so cancel it out. glm::mat3 tmp; tmp[0] = matrix[0] * 1.0f / scaleOut->x; tmp[1] = matrix[1] * 1.0f / scaleOut->y; tmp[2] = matrix[2] * 1.0f / scaleOut->z; *rotationOut = glm::normalize(glm::quat_cast(tmp)); } void DecomposeSwingTwist(const glm::quat& rotation, const glm::vec3& twistAxis, glm::quat* swingOut, glm::quat* twistOut) { glm::vec3 d = glm::normalize(twistAxis); // the twist part has an axis (imaginary component) that is parallel to twistAxis argument glm::vec3 axisOfRotation(rotation.x, rotation.y, rotation.z); glm::vec3 twistImaginaryPart = glm::dot(d, axisOfRotation) * d; // and a real component that is relatively proportional to rotation's real component *twistOut = glm::normalize(glm::quat(rotation.w, twistImaginaryPart.x, twistImaginaryPart.y, twistImaginaryPart.z)); // once twist is known we can solve for swing: // rotation = swing * twist --> swing = rotation * invTwist *swingOut = rotation * glm::inverse(*twistOut); } glm::vec3 XformPoint(const glm::mat4& m, const glm::vec3& p) { glm::vec4 temp(p, 1.0f); glm::vec4 result = m * temp; return glm::vec3(result.x / result.w, result.y / result.w, result.z / result.w); } glm::vec3 XformVec(const glm::mat4& m, const glm::vec3& v) { return glm::mat3(m) * v; } glm::vec3 RandomColor() { return glm::vec3(glm::linearRand(0.0f, 1.0f), glm::linearRand(0.0f, 1.0f), glm::linearRand(0.0f, 1.0f)); } void PrintMat(const glm::mat4& m4, const std::string& name) { Log::D("%s =\n", name.c_str()); Log::D(" | %10.5f, %10.5f, %10.5f, %10.5f |\n", m4[0][0], m4[1][0], m4[2][0], m4[3][0]); Log::D(" | %10.5f, %10.5f, %10.5f, %10.5f |\n", m4[0][1], m4[1][1], m4[2][1], m4[3][1]); Log::D(" | %10.5f, %10.5f, %10.5f, %10.5f |\n", m4[0][2], m4[1][2], m4[2][2], m4[3][2]); Log::D(" | %10.5f, %10.5f, %10.5f, %10.5f |\n", m4[0][3], m4[1][3], m4[2][3], m4[3][3]); } void PrintMat(const glm::mat3& m3, const std::string& name) { Log::D("%s =\n", name.c_str()); Log::D(" | %10.5f, %10.5f, %10.5f |\n", m3[0][0], m3[1][0], m3[2][0]); Log::D(" | %10.5f, %10.5f, %10.5f |\n", m3[0][1], m3[1][1], m3[2][1]); Log::D(" | %10.5f, %10.5f, %10.5f |\n", m3[0][2], m3[1][2], m3[2][2]); } void PrintMat(const glm::mat2& m2, const std::string& name) { Log::D("%s =\n", name.c_str()); Log::D(" | %10.5f, %10.5f |\n", m2[0][0], m2[1][0]); Log::D(" | %10.5f, %10.5f |\n", m2[0][1], m2[1][1]); } void PrintVec(const glm::vec4& v4, const std::string& name) { Log::D("%s = ( %.5f, %.5f, %.5f, %.5f )\n", name.c_str(), v4.x, v4.y, v4.z, v4.w); } void PrintVec(const glm::vec3& v3, const std::string& name) { Log::D("%s = ( %.5f, %.5f, %.5f )\n", name.c_str(), v3.x, v3.y, v3.z); } void PrintVec(const glm::vec2& v2, const std::string& name) { Log::D("%s = ( %.5f, %.5f )\n", name.c_str(), v2.x, v2.y); } void PrintQuat(const glm::quat& q, const std::string& name) { Log::D("%s = ( %.5f, ( %.5f, %.5f, %.5f ) )\n", name.c_str(), q.x, q.y, q.z, q.w); } #ifdef SHIPPING static std::string rootPath(""); #else #ifdef __linux__ // enables us to run from the build dir static std::string rootPath("../"); #else // enables us to run from the build/Debug dir static std::string rootPath("../../"); #endif #endif const std::string& GetRootPath() { return rootPath; } void SetRootPath(const std::string& rootPathIn) { rootPath = rootPathIn; } bool PointInsideAABB(const glm::vec3& point, const glm::vec3& aabbMin, const glm::vec3& aabbMax) { return (point.x >= aabbMin.x && point.x <= aabbMax.x) && (point.y >= aabbMin.y && point.y <= aabbMax.y) && (point.z >= aabbMin.z && point.z <= aabbMax.z); } float LinearToSRGB(float linear) { if (linear <= 0.0031308f) { return 12.92f * linear; } else { return 1.055f * glm::pow(linear, 1.0f / 2.4f) - 0.055f; } } float SRGBToLinear(float srgb) { if (srgb <= 0.04045f) { return srgb / 12.92f; } else { return glm::pow((srgb + 0.055f) / 1.055f, 2.4f); } } glm::vec4 LinearToSRGB(const glm::vec4& linearColor) { glm::vec4 sRGBColor; for (int i = 0; i < 3; ++i) { sRGBColor[i] = LinearToSRGB(linearColor[i]); } sRGBColor.a = linearColor.a; return sRGBColor; } glm::vec4 SRGBToLinear(const glm::vec4& srgbColor) { glm::vec4 linearColor; for (int i = 0; i < 3; ++i) // Convert RGB, leave A unchanged { linearColor[i] = SRGBToLinear(srgbColor[i]); } linearColor.a = srgbColor.a; // Copy alpha channel directly return linearColor; } glm::mat4 MakeRotateAboutPointMat(const glm::vec3& pos, const glm::quat& rot) { glm::mat4 posMat = MakeMat4(glm::quat(), pos); glm::mat4 invPosMat = MakeMat4(glm::quat(), -pos); glm::mat4 rotMat = MakeMat4(rot); return posMat * rotMat * invPosMat; } // 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 void CreateProjection(float* m, 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 m[0] = 2 / tanAngleWidth; m[4] = 0; m[8] = (tanAngleRight + tanAngleLeft) / tanAngleWidth; m[12] = 0; m[1] = 0; m[5] = 2 / tanAngleHeight; m[9] = (tanAngleUp + tanAngleDown) / tanAngleHeight; m[13] = 0; m[2] = 0; m[6] = 0; m[10] = -1; m[14] = -(nearZ + offsetZ); m[3] = 0; m[7] = 0; m[11] = -1; m[15] = 0; } else { // normal projection m[0] = 2 / tanAngleWidth; m[4] = 0; m[8] = (tanAngleRight + tanAngleLeft) / tanAngleWidth; m[12] = 0; m[1] = 0; m[5] = 2 / tanAngleHeight; m[9] = (tanAngleUp + tanAngleDown) / tanAngleHeight; m[13] = 0; m[2] = 0; m[6] = 0; m[10] = -(farZ + offsetZ) / (farZ - nearZ); m[14] = -(farZ * (nearZ + offsetZ)) / (farZ - nearZ); m[3] = 0; m[7] = 0; m[11] = -1; m[15] = 0; } } void StrCpy_s(char* dest, size_t destsz, const char* src) { #ifdef WIN32 strcpy_s(dest, destsz, src); #else strcpy(dest, src); #endif } ================================================ FILE: src/core/util.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // helper functions #pragma once #include #include #include // returns true on success, false on failure bool LoadFile(const std::string& filename, std::string& result); bool SaveFile(const std::string& filename, const std::string& data); // Iterate over codepoints in a utf-8 encoded string int NextCodePointUTF8(const char *str, uint32_t *codePointOut); #ifndef NDEBUG #define GL_ERROR_CHECK(x) GLErrorCheck(x) void GLErrorCheck(const char* message); #else #define GL_ERROR_CHECK(x) #endif glm::vec3 SafeNormalize(const glm::vec3& v, const glm::vec3& ifZero); glm::quat SafeMix(const glm::quat& a, const glm::quat& b, float alpha); glm::mat3 MakeMat3(const glm::quat& rotation); glm::mat3 MakeMat3(const glm::vec3& scale, const glm::quat& rotation); glm::mat4 MakeMat3(float scale, const glm::quat& rotation); glm::mat4 MakeMat4(const glm::vec3& scale, const glm::quat& rotation, const glm::vec3& translation); glm::mat4 MakeMat4(float scale, const glm::quat& rotation, const glm::vec3& translation); glm::mat4 MakeMat4(const glm::quat& rotation, const glm::vec3& translation); glm::mat4 MakeMat4(const glm::quat& rotation); glm::mat4 MakeMat4(const glm::mat3& m3, const glm::vec3& translation); glm::mat4 MakeMat4(const glm::mat3& m3); void Decompose(const glm::mat4& matrix, glm::vec3* scaleOut, glm::quat* rotationOut, glm::vec3* translationOut); void Decompose(const glm::mat3& matrix, glm::vec3* scaleOut, glm::quat* rotationOut); void DecomposeSwingTwist(const glm::quat& rotation, const glm::vec3& twistAxis, glm::quat* swingOut, glm::quat* twistOut); glm::vec3 XformPoint(const glm::mat4& m, const glm::vec3& p); glm::vec3 XformVec(const glm::mat4& m, const glm::vec3& v); glm::vec3 RandomColor(); void PrintMat(const glm::mat4& m4, const std::string& name); void PrintMat(const glm::mat3& m3, const std::string& name); void PrintMat(const glm::mat2& m2, const std::string& name); void PrintVec(const glm::vec4& v4, const std::string& name); void PrintVec(const glm::vec3& v3, const std::string& name); void PrintVec(const glm::vec2& v2, const std::string& name); void PrintQuat(const glm::quat& q, const std::string& name); const std::string& GetRootPath(); void SetRootPath(const std::string& rootPathIn); bool PointInsideAABB(const glm::vec3& point, const glm::vec3& aabbMin, const glm::vec3& aabbMax); float LinearToSRGB(float linear); float SRGBToLinear(float srgb); glm::vec4 LinearToSRGB(const glm::vec4& linearColor); glm::vec4 SRGBToLinear(const glm::vec4& srgbColor); glm::mat4 MakeRotateAboutPointMat(const glm::vec3& pos, const glm::quat& rot); enum GraphicsAPI { GRAPHICS_VULKAN, GRAPHICS_OPENGL, GRAPHICS_OPENGL_ES, GRAPHICS_D3D }; void CreateProjection(float* m, GraphicsAPI graphicsApi, const float tanAngleLeft, const float tanAngleRight, const float tanAngleUp, float const tanAngleDown, const float nearZ, const float farZ); void StrCpy_s(char* dest, size_t destsz, const char* src); ================================================ FILE: src/core/vertexbuffer.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "vertexbuffer.h" #include #include #ifdef __ANDROID__ #include #include #include #include #else #include #define GL_GLEXT_PROTOTYPES 1 #include #include #endif #include "util.h" #ifdef __ANDROID__ static void glBufferStorage(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) { GLenum usage = 0; if (flags & GL_DYNAMIC_STORAGE_BIT) { if (flags & GL_MAP_READ_BIT) { usage = GL_DYNAMIC_READ; } else { usage = GL_DYNAMIC_DRAW; } } else { if (flags & GL_MAP_READ_BIT) { usage = GL_STATIC_READ; } else { usage = GL_STATIC_DRAW; } } glBufferData(target, size, data, usage); } #endif BufferObject::BufferObject(int targetIn, void* data, size_t size, unsigned int flags) { target = targetIn; glGenBuffers(1, &obj); Bind(); glBufferStorage(target, size, data, flags); Unbind(); elementSize = 0; numElements = 0; } BufferObject::BufferObject(int targetIn, const std::vector& data, unsigned int flags) { target = targetIn; glGenBuffers(1, &obj); Bind(); glBufferStorage(target, sizeof(float) * data.size(), (void*)data.data(), flags); Unbind(); elementSize = 1; numElements = (int)data.size(); } BufferObject::BufferObject(int targetIn, const std::vector& data, unsigned int flags) { target = targetIn; glGenBuffers(1, &obj); Bind(); glBufferStorage(target, sizeof(glm::vec2) * data.size(), (void*)data.data(), flags); Unbind(); elementSize = 2; numElements = (int)data.size(); } BufferObject::BufferObject(int targetIn, const std::vector& data, unsigned int flags) { target = targetIn; glGenBuffers(1, &obj); Bind(); glBufferStorage(target, sizeof(glm::vec3) * data.size(), (void*)data.data(), flags); Unbind(); elementSize = 3; numElements = (int)data.size(); } BufferObject::BufferObject(int targetIn, const std::vector& data, unsigned int flags) { target = targetIn; glGenBuffers(1, &obj); Bind(); glBufferStorage(target, sizeof(glm::vec4) * data.size(), (void*)data.data(), flags); Unbind(); elementSize = 4; numElements = (int)data.size(); } BufferObject::BufferObject(int targetIn, const std::vector& data, unsigned int flags) { target = targetIn; glGenBuffers(1, &obj); Bind(); glBufferStorage(target, sizeof(uint32_t) * data.size(), (void*)data.data(), flags); Unbind(); elementSize = 1; numElements = (int)data.size(); } BufferObject::~BufferObject() { glDeleteBuffers(1, &obj); } void BufferObject::Bind() const { glBindBuffer(target, obj); } void BufferObject::Unbind() const { glBindBuffer(target, 0); } void BufferObject::Update(const std::vector& data) { Bind(); glBufferSubData(target, 0, sizeof(float) * data.size(), (void*)data.data()); Unbind(); } void BufferObject::Update(const std::vector& data) { Bind(); glBufferSubData(target, 0, sizeof(glm::vec2) * data.size(), (void*)data.data()); Unbind(); } void BufferObject::Update(const std::vector& data) { Bind(); glBufferSubData(target, 0, sizeof(glm::vec3) * data.size(), (void*)data.data()); Unbind(); } void BufferObject::Update(const std::vector& data) { Bind(); glBufferSubData(target, 0, sizeof(glm::vec4) * data.size(), (void*)data.data()); Unbind(); } void BufferObject::Update(const std::vector& data) { Bind(); glBufferSubData(target, 0, sizeof(uint32_t) * data.size(), (void*)data.data()); Unbind(); } void BufferObject::Read(std::vector& data) { Bind(); size_t bufferSize = sizeof(uint32_t) * data.size(); assert(bufferSize == (elementSize * sizeof(uint32_t) * numElements)); //void* rawBuffer = glMapBuffer(target, GL_READ_ONLY); void* rawBuffer = glMapBufferRange(target, 0, bufferSize, GL_MAP_READ_BIT); if (rawBuffer) { memcpy((void*)data.data(), rawBuffer, bufferSize); } glUnmapBuffer(target); Unbind(); } VertexArrayObject::VertexArrayObject() { glGenVertexArrays(1, &obj); } VertexArrayObject::~VertexArrayObject() { glDeleteVertexArrays(1, &obj); } void VertexArrayObject::Bind() const { glBindVertexArray(obj); } void VertexArrayObject::Unbind() const { glBindVertexArray(0); } void VertexArrayObject::SetAttribBuffer(int loc, std::shared_ptr attribBuffer) { assert(attribBuffer->target == GL_ARRAY_BUFFER); Bind(); attribBuffer->Bind(); glVertexAttribPointer(loc, attribBuffer->elementSize, GL_FLOAT, GL_FALSE, 0, nullptr); glEnableVertexAttribArray(loc); attribBuffer->Unbind(); attribBufferVec.push_back(attribBuffer); Unbind(); } void VertexArrayObject::SetElementBuffer(std::shared_ptr elementBufferIn) { assert(elementBufferIn->target == GL_ELEMENT_ARRAY_BUFFER); elementBuffer = elementBufferIn; Bind(); elementBufferIn->Bind(); Unbind(); } void VertexArrayObject::DrawElements(int mode) const { Bind(); glDrawElements((GLenum)mode, elementBuffer->numElements, GL_UNSIGNED_INT, nullptr); Unbind(); } ================================================ FILE: src/core/vertexbuffer.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include class VertexArrayObject; #ifdef __ANDROID__ // AJT: ANDROID: TODO: HACK TO WORK AROUND glBufferStorage #define GL_DYNAMIC_STORAGE_BIT 0x0100 #define GL_MAP_READ_BIT 0x0001 #endif class BufferObject { friend class VertexArrayObject; public: // targetIn should be one of the following. // GL_ARRAY_BUFFER, GL_ATOMIC_COUNTER_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER // GL_DISPATCH_INDIRECT_BUFFER, GL_DRAW_INDIRECT_BUFFER, GL_ELEMENT_ARRAY_BUFFER, // GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER, GL_QUERY_BUFFER, GL_SHADER_STORAGE_BUFFER, // GL_TEXTURE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER, GL_UNIFORM_BUFFER // flags can one of the following bitfields. // GL_DYNAMIC_STORAGE_BIT, GL_MAP_READ_BIT, GL_MAP_WRITE_BIT, GL_MAP_PERSISTENT_BIT // GL_MAP_COHERENT_BIT, GL_CLIENT_STORAGE_BIT BufferObject(int targetIn, void* data, size_t size, unsigned int flags = 0); BufferObject(int targetIn, const std::vector& data, unsigned int flags = 0); BufferObject(int targetIn, const std::vector& data, unsigned int flags = 0); BufferObject(int targetIn, const std::vector& data, unsigned int flags = 0); BufferObject(int targetIn, const std::vector& data, unsigned int flags = 0); BufferObject(int targetIn, const std::vector& data, unsigned int flags = 0); BufferObject(const BufferObject& orig) = delete; ~BufferObject(); void Bind() const; void Unbind() const; void Update(const std::vector& data); void Update(const std::vector& data); void Update(const std::vector& data); void Update(const std::vector& data); void Update(const std::vector& data); void Read(std::vector& data); uint32_t GetObj() const { return obj; } protected: int target; uint32_t obj; int elementSize; // vec2 = 2, vec3 = 3 etc. int numElements; // number of vec2, vec3 in buffer }; class VertexArrayObject { public: VertexArrayObject(); ~VertexArrayObject(); void Bind() const; void Unbind() const; void SetAttribBuffer(int loc, std::shared_ptr attribBufferIn); void SetElementBuffer(std::shared_ptr elementBufferIn); std::shared_ptr GetElementBuffer() const { return elementBuffer; } void DrawElements(int mode) const; protected: uint32_t obj; std::vector> attribBufferVec; std::shared_ptr elementBuffer; }; ================================================ FILE: src/core/xrbuddy.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "xrbuddy.h" #include #include #include #ifndef __ANDROID__ #include #endif #include #ifdef TRACY_ENABLE #include #else #define ZoneScoped #define ZoneScopedNC(NAME, COLOR) #endif #include "log.h" #include "util.h" static bool printAll = true; struct PathCache { PathCache(XrInstance instanceIn) : instance(instanceIn) {} XrPath operator[](const std::string& key) { auto iter = pathMap.find(key); if (iter != pathMap.end()) { return iter->second; } else { XrPath path = XR_NULL_PATH; xrStringToPath(instance, key.c_str(), &path); pathMap.insert({ key, path }); return path; } } protected: XrInstance instance; std::map pathMap; }; static bool CheckResult(XrInstance instance, XrResult result, const char* str) { if (XR_SUCCEEDED(result)) { return true; } if (instance != XR_NULL_HANDLE) { char resultString[XR_MAX_RESULT_STRING_SIZE]; xrResultToString(instance, result, resultString); Log::E("%s [%s]\n", str, resultString); } else { Log::E("%s\n", str); } return false; } static bool EnumerateExtensions(std::vector& extensionProps) { XrResult result; uint32_t extensionCount = 0; result = xrEnumerateInstanceExtensionProperties(NULL, 0, &extensionCount, NULL); if (!CheckResult(NULL, result, "xrEnumerateInstanceExtensionProperties failed")) { return false; } extensionProps.resize(extensionCount); for (uint32_t i = 0; i < extensionCount; i++) { extensionProps[i].type = XR_TYPE_EXTENSION_PROPERTIES; extensionProps[i].next = NULL; } result = xrEnumerateInstanceExtensionProperties(NULL, extensionCount, &extensionCount, extensionProps.data()); if (!CheckResult(NULL, result, "xrEnumerateInstanceExtensionProperties failed")) { return false; } bool printExtensions = false; if (printExtensions || printAll) { Log::D("%d extensions:\n", extensionCount); for (uint32_t i = 0; i < extensionCount; ++i) { Log::D(" %s\n", extensionProps[i].extensionName); } } return true; } static bool ExtensionSupported(const std::vector& extensions, const char* extensionName) { bool supported = false; for (auto& extension : extensions) { if (!strcmp(extensionName, extension.extensionName)) { supported = true; } } return supported; } static bool EnumerateApiLayers(std::vector& layerProps) { uint32_t layerCount; XrResult result = xrEnumerateApiLayerProperties(0, &layerCount, NULL); if (!CheckResult(NULL, result, "xrEnumerateApiLayerProperties")) { return false; } layerProps.resize(layerCount); for (uint32_t i = 0; i < layerCount; i++) { layerProps[i].type = XR_TYPE_API_LAYER_PROPERTIES; layerProps[i].next = NULL; } result = xrEnumerateApiLayerProperties(layerCount, &layerCount, layerProps.data()); if (!CheckResult(NULL, result, "xrEnumerateApiLayerProperties")) { return false; } bool printLayers = false; if (printLayers || printAll) { Log::D("%d XrApiLayerProperties:\n", layerCount); for (uint32_t i = 0; i < layerCount; i++) { Log::D(" %s, %s\n", layerProps[i].layerName, layerProps[i].description); } } return true; } static bool CreateInstance(XrInstance& instance, const std::vector& extensionVec) { if (printAll) { Log::D("Attempting to enable the following extensions:\n"); for (auto&& ext : extensionVec) { Log::D(" %s\n", ext); } } // create openxr instance XrResult result; XrInstanceCreateInfo ici = {}; ici.type = XR_TYPE_INSTANCE_CREATE_INFO; ici.next = NULL; ici.createFlags = 0; ici.enabledExtensionCount = (uint32_t)extensionVec.size(); ici.enabledExtensionNames = extensionVec.data(); ici.enabledApiLayerCount = 0; ici.enabledApiLayerNames = NULL; StrCpy_s(ici.applicationInfo.applicationName, XR_MAX_APPLICATION_NAME_SIZE, "xrtoy"); ici.applicationInfo.engineName[0] = '\0'; ici.applicationInfo.applicationVersion = 1; ici.applicationInfo.engineVersion = 0; ici.applicationInfo.apiVersion = XR_CURRENT_API_VERSION; result = xrCreateInstance(&ici, &instance); if (!CheckResult(NULL, result, "xrCreateInstance failed")) { return false; } bool printRuntimeInfo = false; if (printRuntimeInfo || printAll) { XrInstanceProperties ip = {}; ip.type = XR_TYPE_INSTANCE_PROPERTIES; ip.next = NULL; result = xrGetInstanceProperties(instance, &ip); if (!CheckResult(instance, result, "xrGetInstanceProperties failed")) { return false; } Log::D("Runtime Name: %s\n", ip.runtimeName); Log::D("Runtime Version: %d.%d.%d\n", XR_VERSION_MAJOR(ip.runtimeVersion), XR_VERSION_MINOR(ip.runtimeVersion), XR_VERSION_PATCH(ip.runtimeVersion)); } return true; } static bool GetSystemId(XrInstance instance, XrSystemId& systemId) { XrResult result; XrSystemGetInfo sgi = {}; sgi.type = XR_TYPE_SYSTEM_GET_INFO; sgi.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; sgi.next = NULL; result = xrGetSystem(instance, &sgi, &systemId); if (!CheckResult(instance, result, "xrGetSystemFailed")) { return false; } return true; } static bool GetSystemProperties(XrInstance instance, XrSystemId systemId, XrSystemProperties& sp) { XrResult result; sp.type = XR_TYPE_SYSTEM_PROPERTIES; sp.next = NULL; sp.graphicsProperties = {0}; sp.trackingProperties = {0}; result = xrGetSystemProperties(instance, systemId, &sp); if (!CheckResult(instance, result, "xrGetSystemProperties failed")) { return false; } bool printSystemProperties = false; if (printSystemProperties || printAll) { Log::D("System properties for system \"%s\":\n", sp.systemName); Log::D(" maxLayerCount: %d\n", sp.graphicsProperties.maxLayerCount); Log::D(" maxSwapChainImageHeight: %d\n", sp.graphicsProperties.maxSwapchainImageHeight); Log::D(" maxSwapChainImageWidth: %d\n", sp.graphicsProperties.maxSwapchainImageWidth); Log::D(" Orientation Tracking: %s\n", sp.trackingProperties.orientationTracking ? "true" : "false"); Log::D(" Position Tracking: %s\n", sp.trackingProperties.positionTracking ? "true" : "false"); } return true; } static bool SupportsVR(XrInstance instance, XrSystemId systemId) { XrResult result; uint32_t viewConfigurationCount; result = xrEnumerateViewConfigurations(instance, systemId, 0, &viewConfigurationCount, NULL); if (!CheckResult(instance, result, "xrEnumerateViewConfigurations")) { return false; } std::vector viewConfigurations(viewConfigurationCount); result = xrEnumerateViewConfigurations(instance, systemId, viewConfigurationCount, &viewConfigurationCount, viewConfigurations.data()); if (!CheckResult(instance, result, "xrEnumerateViewConfigurations")) { return false; } XrViewConfigurationType stereoViewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; for (uint32_t i = 0; i < viewConfigurationCount; i++) { XrViewConfigurationProperties vcp = {}; vcp.type = XR_TYPE_VIEW_CONFIGURATION_PROPERTIES; vcp.next = NULL; result = xrGetViewConfigurationProperties(instance, systemId, viewConfigurations[i], &vcp); if (!CheckResult(instance, result, "xrGetViewConfigurationProperties")) { return false; } if (viewConfigurations[i] == stereoViewConfigType && vcp.viewConfigurationType == stereoViewConfigType) { return true; } } return false; } static bool EnumerateViewConfigs(XrInstance instance, XrSystemId systemId, std::vector& viewConfigs) { XrResult result; uint32_t viewCount; XrViewConfigurationType stereoViewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; result = xrEnumerateViewConfigurationViews(instance, systemId, stereoViewConfigType, 0, &viewCount, NULL); if (!CheckResult(instance, result, "xrEnumerateViewConfigurationViews")) { return false; } viewConfigs.resize(viewCount); for (uint32_t i = 0; i < viewCount; i++) { viewConfigs[i].type = XR_TYPE_VIEW_CONFIGURATION_VIEW; viewConfigs[i].next = NULL; } result = xrEnumerateViewConfigurationViews(instance, systemId, stereoViewConfigType, viewCount, &viewCount, viewConfigs.data()); if (!CheckResult(instance, result, "xrEnumerateViewConfigurationViews")) { return false; } bool printViews = false; if (printViews || printAll) { Log::D("%d viewConfigs:\n", viewCount); for (uint32_t i = 0; i < viewCount; i++) { Log::D(" viewConfigs[%d]:\n", i); Log::D(" recommendedImageRectWidth: %d\n", viewConfigs[i].recommendedImageRectWidth); Log::D(" maxImageRectWidth: %d\n", viewConfigs[i].maxImageRectWidth); Log::D(" recommendedImageRectHeight: %d\n", viewConfigs[i].recommendedImageRectHeight); Log::D(" maxImageRectHeight: %d\n", viewConfigs[i].maxImageRectHeight); Log::D(" recommendedSwapchainSampleCount: %d\n", viewConfigs[i].recommendedSwapchainSampleCount); Log::D(" maxSwapchainSampleCount: %d\n", viewConfigs[i].maxSwapchainSampleCount); } } return true; } static bool SetColorSpace(XrInstance instance, XrSession session, XrColorSpaceFB colorSpace) { XrResult result; PFN_xrSetColorSpaceFB xrSetColorSpaceFB = nullptr; result = xrGetInstanceProcAddr(instance, "xrSetColorSpaceFB", (PFN_xrVoidFunction*)&xrSetColorSpaceFB); if (!CheckResult(instance, result, "xrGetInstanceProcAddr(xrSetColorSpaceFB)") || !xrSetColorSpaceFB) { Log::W("get proc addr xrSetColorSpaceFB() failed\n"); return false; } result = xrSetColorSpaceFB(session, colorSpace); if (!CheckResult(instance, result, "xrSetColorSpaceFB")) { Log::W("xrSetColorSpaceFB(XR_COLOR_SPACE_REC2020_FB) failed\n"); return false; } return true; } static bool CreateSession(XrInstance instance, XrSystemId systemId, XrSession& session, const MainContext& mainContext) { XrResult result; // check if opengl version is sufficient. #ifdef XR_USE_GRAPHICS_API_OPENGL { XrGraphicsRequirementsOpenGLKHR reqs = {}; reqs.type = XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR; reqs.next = NULL; PFN_xrGetOpenGLGraphicsRequirementsKHR pfnGetOpenGLGraphicsRequirementsKHR = NULL; result = xrGetInstanceProcAddr(instance, "xrGetOpenGLGraphicsRequirementsKHR", (PFN_xrVoidFunction *)&pfnGetOpenGLGraphicsRequirementsKHR); if (!CheckResult(instance, result, "xrGetInstanceProcAddr")) { return false; } result = pfnGetOpenGLGraphicsRequirementsKHR(instance, systemId, &reqs); if (!CheckResult(instance, result, "GetOpenGLGraphicsRequirementsPKR")) { return false; } GLint major = 0; GLint minor = 0; glGetIntegerv(GL_MAJOR_VERSION, &major); glGetIntegerv(GL_MINOR_VERSION, &minor); const XrVersion desiredApiVersion = XR_MAKE_VERSION(major, minor, 0); bool printVersion = false; if (printVersion || printAll) { Log::D("current OpenGL version: %d.%d.%d\n", XR_VERSION_MAJOR(desiredApiVersion), XR_VERSION_MINOR(desiredApiVersion), XR_VERSION_PATCH(desiredApiVersion)); Log::D("minimum OpenGL version: %d.%d.%d\n", XR_VERSION_MAJOR(reqs.minApiVersionSupported), XR_VERSION_MINOR(reqs.minApiVersionSupported), XR_VERSION_PATCH(reqs.minApiVersionSupported)); } if (reqs.minApiVersionSupported > desiredApiVersion) { Log::E("Runtime does not support desired Graphics API and/or version\n"); return false; } } #ifdef WIN32 XrGraphicsBindingOpenGLWin32KHR glBinding = {}; glBinding.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR; glBinding.next = NULL; glBinding.hDC = wglGetCurrentDC(); glBinding.hGLRC = wglGetCurrentContext(); #else XrGraphicsBindingOpenGLXlibKHR glBinding = {}; glBinding.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR; glBinding.next = NULL; glBinding.xDisplay = mainContext.xdisplay; glBinding.glxDrawable = mainContext.glxDrawable; glBinding.glxContext = mainContext.glxContext; #endif #elif defined (XR_USE_GRAPHICS_API_OPENGL_ES) XrGraphicsRequirementsOpenGLESKHR reqs = {}; reqs.type = XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR; reqs.next = NULL; PFN_xrGetOpenGLESGraphicsRequirementsKHR pfnGetOpenGLESGraphicsRequirementsKHR = NULL; result = xrGetInstanceProcAddr(instance, "xrGetOpenGLESGraphicsRequirementsKHR", (PFN_xrVoidFunction *)&pfnGetOpenGLESGraphicsRequirementsKHR); if (!CheckResult(instance, result, "xrGetInstanceProcAddr")) { return false; } result = pfnGetOpenGLESGraphicsRequirementsKHR(instance, systemId, &reqs); if (!CheckResult(instance, result, "GetOpenGLGraphicsRequirementsPKR")) { return false; } GLint major = 0; GLint minor = 0; glGetIntegerv(GL_MAJOR_VERSION, &major); glGetIntegerv(GL_MINOR_VERSION, &minor); const XrVersion desiredApiVersion = XR_MAKE_VERSION(major, minor, 0); bool printVersion = false; if (printVersion || printAll) { Log::D("current OpenGLES version: %d.%d.%d\n", XR_VERSION_MAJOR(desiredApiVersion), XR_VERSION_MINOR(desiredApiVersion), XR_VERSION_PATCH(desiredApiVersion)); Log::D("minimum OpenGLES version: %d.%d.%d\n", XR_VERSION_MAJOR(reqs.minApiVersionSupported), XR_VERSION_MINOR(reqs.minApiVersionSupported), XR_VERSION_PATCH(reqs.minApiVersionSupported)); } if (reqs.minApiVersionSupported > desiredApiVersion) { Log::E("Runtime does not support desired Graphics API and/or version\n"); return false; } XrGraphicsBindingOpenGLESAndroidKHR glBinding = {}; glBinding.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR; glBinding.next = NULL; glBinding.display = mainContext.display; glBinding.config = mainContext.config; glBinding.context = mainContext.context; #endif XrSessionCreateInfo sci = {}; sci.type = XR_TYPE_SESSION_CREATE_INFO; sci.next = &glBinding; sci.createFlags = 0; sci.systemId = systemId; result = xrCreateSession(instance, &sci, &session); if (!CheckResult(instance, result, "xrCreateSession")) { return false; } return true; } static bool CreateActions(XrInstance instance, XrSystemId systemId, XrSession session, XrActionSet& actionSet, std::map& actionMap) { XrResult result; // create action set XrActionSetCreateInfo asci = {}; asci.type = XR_TYPE_ACTION_SET_CREATE_INFO; asci.next = NULL; StrCpy_s(asci.actionSetName, XR_MAX_ACTION_NAME_SIZE, "default"); StrCpy_s(asci.localizedActionSetName, XR_MAX_LOCALIZED_ACTION_NAME_SIZE, "Default"); asci.priority = 0; result = xrCreateActionSet(instance, &asci, &actionSet); if (!CheckResult(instance, result, "xrCreateActionSet")) { return false; } std::vector> actionPairVec = { {"l_select_click", XR_ACTION_TYPE_BOOLEAN_INPUT}, {"r_select_click", XR_ACTION_TYPE_BOOLEAN_INPUT}, {"l_menu_click", XR_ACTION_TYPE_BOOLEAN_INPUT}, {"r_menu_click", XR_ACTION_TYPE_BOOLEAN_INPUT}, {"l_squeeze_click", XR_ACTION_TYPE_BOOLEAN_INPUT}, {"r_squeeze_click", XR_ACTION_TYPE_BOOLEAN_INPUT}, {"l_trackpad_click", XR_ACTION_TYPE_BOOLEAN_INPUT}, {"r_trackpad_click", XR_ACTION_TYPE_BOOLEAN_INPUT}, {"l_trackpad_x", XR_ACTION_TYPE_FLOAT_INPUT}, {"r_trackpad_x", XR_ACTION_TYPE_FLOAT_INPUT}, {"l_trackpad_y", XR_ACTION_TYPE_FLOAT_INPUT}, {"r_trackpad_y", XR_ACTION_TYPE_FLOAT_INPUT}, {"l_grip_pose", XR_ACTION_TYPE_POSE_INPUT}, {"r_grip_pose", XR_ACTION_TYPE_POSE_INPUT}, {"l_aim_pose", XR_ACTION_TYPE_POSE_INPUT}, {"r_aim_pose", XR_ACTION_TYPE_POSE_INPUT}, {"l_haptic", XR_ACTION_TYPE_VIBRATION_OUTPUT}, {"r_haptic", XR_ACTION_TYPE_VIBRATION_OUTPUT}, {"l_stick", XR_ACTION_TYPE_VECTOR2F_INPUT}, {"r_stick", XR_ACTION_TYPE_VECTOR2F_INPUT} }; for (auto& actionPair : actionPairVec) { // selectAction XrAction action = XR_NULL_HANDLE; XrActionCreateInfo aci = {}; aci.type = XR_TYPE_ACTION_CREATE_INFO; aci.next = NULL; aci.actionType = actionPair.second; StrCpy_s(aci.actionName, XR_MAX_ACTION_NAME_SIZE, actionPair.first.c_str()); StrCpy_s(aci.localizedActionName, XR_MAX_LOCALIZED_ACTION_NAME_SIZE, actionPair.first.c_str()); aci.countSubactionPaths = 0; aci.subactionPaths = NULL; result = xrCreateAction(actionSet, &aci, &action); if (!CheckResult(instance, result, "xrCreateAction")) { return false; } XrSpace space = XR_NULL_HANDLE; if (actionPair.second == XR_ACTION_TYPE_POSE_INPUT) { XrActionSpaceCreateInfo aspci = {}; aspci.type = XR_TYPE_ACTION_SPACE_CREATE_INFO; aspci.next = NULL; aspci.action = action; XrPosef identity; identity.orientation = {0.0f, 0.0f, 0.0f, 1.0f}; identity.position = {0.0f, 0.0f, 0.0f}; aspci.poseInActionSpace = identity; aspci.subactionPath = 0; result = xrCreateActionSpace(session, &aspci, &space); if (!CheckResult(instance, result, "xrCreateActionSpace")) { return false; } } actionMap[actionPair.first] = XrBuddy::ActionInfo(action, actionPair.second, space); } PathCache pathCache(instance); if (!CheckResult(instance, result, "xrStringToPath")) { return false; } // KHR Simple { XrPath interactionProfilePath = XR_NULL_PATH; xrStringToPath(instance, "/interaction_profiles/khr/simple_controller", &interactionProfilePath); std::vector bindings = { {actionMap["l_select_click"].action, pathCache["/user/hand/left/input/select/click"]}, {actionMap["r_select_click"].action, pathCache["/user/hand/right/input/select/click"]}, {actionMap["l_menu_click"].action, pathCache["/user/hand/left/input/menu/click"]}, {actionMap["r_menu_click"].action, pathCache["/user/hand/right/input/menu/click"]}, {actionMap["l_grip_pose"].action, pathCache["/user/hand/left/input/grip/pose"]}, {actionMap["r_grip_pose"].action, pathCache["/user/hand/right/input/grip/pose"]}, {actionMap["l_aim_pose"].action, pathCache["/user/hand/left/input/aim/pose"]}, {actionMap["r_aim_pose"].action, pathCache["/user/hand/right/input/aim/pose"]}, {actionMap["l_haptic"].action, pathCache["/user/hand/left/output/haptic"]}, {actionMap["r_haptic"].action, pathCache["/user/hand/right/output/haptic"]} }; XrInteractionProfileSuggestedBinding suggestedBindings = {}; suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING; suggestedBindings.next = NULL; suggestedBindings.interactionProfile = interactionProfilePath; suggestedBindings.suggestedBindings = bindings.data(); suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size(); result = xrSuggestInteractionProfileBindings(instance, &suggestedBindings); if (!CheckResult(instance, result, "xrSuggestInteractionProfileBindings")) { return false; } } // oculus touch { XrPath interactionProfilePath = XR_NULL_PATH; xrStringToPath(instance, "/interaction_profiles/oculus/touch_controller", &interactionProfilePath); std::vector bindings = { {actionMap["l_select_click"].action, pathCache["/user/hand/left/input/trigger/value"]}, {actionMap["r_select_click"].action, pathCache["/user/hand/right/input/trigger/value"]}, {actionMap["l_menu_click"].action, pathCache["/user/hand/left/input/menu/click"]}, //{actionMap["r_menu_click"].action, pathCache["/user/hand/right/input/menu/click"]}, // right controller has no menu button {actionMap["l_squeeze_click"].action, pathCache["/user/hand/left/input/squeeze/value"]}, {actionMap["r_squeeze_click"].action, pathCache["/user/hand/right/input/squeeze/value"]}, {actionMap["l_grip_pose"].action, pathCache["/user/hand/left/input/grip/pose"]}, {actionMap["r_grip_pose"].action, pathCache["/user/hand/right/input/grip/pose"]}, {actionMap["l_aim_pose"].action, pathCache["/user/hand/left/input/aim/pose"]}, {actionMap["r_aim_pose"].action, pathCache["/user/hand/right/input/aim/pose"]}, {actionMap["l_haptic"].action, pathCache["/user/hand/left/output/haptic"]}, {actionMap["r_haptic"].action, pathCache["/user/hand/right/output/haptic"]}, {actionMap["l_stick"].action, pathCache["/user/hand/left/input/thumbstick"]}, {actionMap["r_stick"].action, pathCache["/user/hand/right/input/thumbstick"]} }; XrInteractionProfileSuggestedBinding suggestedBindings = {}; suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING; suggestedBindings.next = NULL; suggestedBindings.interactionProfile = interactionProfilePath; suggestedBindings.suggestedBindings = bindings.data(); suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size(); result = xrSuggestInteractionProfileBindings(instance, &suggestedBindings); if (!CheckResult(instance, result, "xrSuggestInteractionProfileBindings (oculus)")) { return false; } } // vive { XrPath interactionProfilePath = XR_NULL_PATH; xrStringToPath(instance, "/interaction_profiles/htc/vive_controller", &interactionProfilePath); std::vector bindings = { {actionMap["l_menu_click"].action, pathCache["/user/hand/left/input/menu/click"]}, {actionMap["r_menu_click"].action, pathCache["/user/hand/right/input/menu/click"]}, {actionMap["l_select_click"].action, pathCache["/user/hand/left/input/trigger/click"]}, {actionMap["r_select_click"].action, pathCache["/user/hand/right/input/trigger/click"]}, {actionMap["l_squeeze_click"].action, pathCache["/user/hand/left/input/squeeze/click"]}, {actionMap["r_squeeze_click"].action, pathCache["/user/hand/right/input/squeeze/click"]}, {actionMap["l_trackpad_click"].action, pathCache["/user/hand/left/input/trackpad/click"]}, {actionMap["r_trackpad_click"].action, pathCache["/user/hand/right/input/trackpad/click"]}, {actionMap["l_trackpad_x"].action, pathCache["/user/hand/left/input/trackpad/x"]}, {actionMap["r_trackpad_x"].action, pathCache["/user/hand/right/input/trackpad/x"]}, {actionMap["l_trackpad_y"].action, pathCache["/user/hand/left/input/trackpad/y"]}, {actionMap["r_trackpad_y"].action, pathCache["/user/hand/right/input/trackpad/y"]}, {actionMap["l_grip_pose"].action, pathCache["/user/hand/left/input/grip/pose"]}, {actionMap["r_grip_pose"].action, pathCache["/user/hand/right/input/grip/pose"]}, {actionMap["l_aim_pose"].action, pathCache["/user/hand/left/input/aim/pose"]}, {actionMap["r_aim_pose"].action, pathCache["/user/hand/right/input/aim/pose"]}, {actionMap["l_haptic"].action, pathCache["/user/hand/left/output/haptic"]}, {actionMap["r_haptic"].action, pathCache["/user/hand/right/output/haptic"]} }; XrInteractionProfileSuggestedBinding suggestedBindings = {}; suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING; suggestedBindings.next = NULL; suggestedBindings.interactionProfile = interactionProfilePath; suggestedBindings.suggestedBindings = bindings.data(); suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size(); result = xrSuggestInteractionProfileBindings(instance, &suggestedBindings); if (!CheckResult(instance, result, "xrSuggestInteractionProfileBindings (vive)")) { return false; } } // TODO #if 0 // microsoft mixed reality { XrPath interactionProfilePath = XR_NULL_PATH; xrStringToPath(instance, "/interaction_profiles/microsoft/motion_controller", &interactionProfilePath); std::vector bindings = { {selectAction, squeezeClickPath[0]}, {selectAction, squeezeClickPath[1]}, {aimAction, aimPath[0]}, {aimAction, aimPath[1]}, {menuAction, menuClickPath[0]}, {menuAction, menuClickPath[1]}, {vibrateAction, hapticPath[0]}, {vibrateAction, hapticPath[1]} }; XrInteractionProfileSuggestedBinding suggestedBindings = {}; suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING; suggestedBindings.next = NULL; suggestedBindings.interactionProfile = interactionProfilePath; suggestedBindings.suggestedBindings = bindings.data(); suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size(); result = xrSuggestInteractionProfileBindings(instance, &suggestedBindings); if (!CheckResult(instance, result, "xrSuggestInteractionProfileBindings")) { return false; } } #endif XrSessionActionSetsAttachInfo sasai = {}; sasai.type = XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO; sasai.next = NULL; sasai.countActionSets = 1; sasai.actionSets = &actionSet; result = xrAttachSessionActionSets(session, &sasai); if (!CheckResult(instance, result, "xrSessionActionSetsAttachInfo")) { return false; } return true; } static bool CreateSpaces(XrInstance instance, XrSystemId systemId, XrSession session, XrSpace& stageSpace, XrSpace& viewSpace, XrSpaceLocation& viewSpaceLocation, XrSpaceVelocity& viewSpaceVelocity) { XrResult result; bool printReferenceSpaces = true; if (printReferenceSpaces || printAll) { uint32_t referenceSpacesCount; result = xrEnumerateReferenceSpaces(session, 0, &referenceSpacesCount, NULL); if (!CheckResult(instance, result, "xrEnumerateReferenceSpaces")) { return false; } std::vector referenceSpaces(referenceSpacesCount, XR_REFERENCE_SPACE_TYPE_VIEW); result = xrEnumerateReferenceSpaces(session, referenceSpacesCount, &referenceSpacesCount, referenceSpaces.data()); if (!CheckResult(instance, result, "xrEnumerateReferenceSpaces")) { return false; } Log::D("referenceSpaces:\n"); for (uint32_t i = 0; i < referenceSpacesCount; i++) { switch (referenceSpaces[i]) { case XR_REFERENCE_SPACE_TYPE_VIEW: Log::D(" XR_REFERENCE_SPACE_TYPE_VIEW\n"); break; case XR_REFERENCE_SPACE_TYPE_LOCAL: Log::D(" XR_REFERENCE_SPACE_TYPE_LOCAL\n"); break; case XR_REFERENCE_SPACE_TYPE_STAGE: Log::D(" XR_REFERENCE_SPACE_TYPE_STAGE\n"); break; default: Log::D(" XR_REFERENCE_SPACE_TYPE_%d\n", referenceSpaces[i]); break; } } } XrPosef identityPose; identityPose.orientation = {0.0f, 0.0f, 0.0f, 1.0f}; identityPose.position = {0.0f, 0.0f, 0.0f}; XrReferenceSpaceCreateInfo rsci = {}; rsci.type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO; rsci.next = NULL; rsci.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; rsci.poseInReferenceSpace = identityPose; result = xrCreateReferenceSpace(session, &rsci, &stageSpace); if (!CheckResult(instance, result, "xrCreateReferenceSpace")) { return false; } rsci.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW; result = xrCreateReferenceSpace(session, &rsci, &viewSpace); if (!CheckResult(instance, result, "xrCreateReferenceSpace")) { return false; } viewSpaceLocation.type = XR_TYPE_SPACE_LOCATION; viewSpaceLocation.next = &viewSpaceVelocity; viewSpaceVelocity.type = XR_TYPE_SPACE_VELOCITY; viewSpaceVelocity.next = NULL; return true; } static bool BeginSession(XrInstance instance, XrSystemId systemId, XrSession session) { XrResult result; XrViewConfigurationType stereoViewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; XrSessionBeginInfo sbi = {}; sbi.type = XR_TYPE_SESSION_BEGIN_INFO; sbi.next = NULL; sbi.primaryViewConfigurationType = stereoViewConfigType; result = xrBeginSession(session, &sbi); if (!CheckResult(instance, result, "xrBeginSession")) { return false; } return true; } static bool EndSession(XrInstance instance, XrSystemId systemId, XrSession session) { XrResult result; result = xrEndSession(session); if (!CheckResult(instance, result, "xrEndSession")) { return false; } return true; } static bool CreateFrameBuffer(GLuint& frameBuffer) { glGenFramebuffers(1, &frameBuffer); return true; } static bool CreateSwapchains(XrInstance instance, XrSession session, const std::vector& viewConfigs, std::vector& swapchains, std::vector>& swapchainImages) { XrResult result; uint32_t swapchainFormatCount; result = xrEnumerateSwapchainFormats(session, 0, &swapchainFormatCount, NULL); if (!CheckResult(instance, result, "xrEnumerateSwapchainFormats")) { return false; } std::vector swapchainFormats(swapchainFormatCount); result = xrEnumerateSwapchainFormats(session, swapchainFormatCount, &swapchainFormatCount, swapchainFormats.data()); if (!CheckResult(instance, result, "xrEnumerateSwapchainFormats")) { return false; } if (printAll) { Log::D("xrEnumerateSwapchainFormats, count = %d\n", swapchainFormatCount); for (uint32_t i = 0; i < swapchainFormatCount; i++) { Log::D(" format[%d] = 0x%x\n", i, swapchainFormats[i]); } } std::vector formats = {GL_R11F_G11F_B10F_EXT, GL_RGB16F_EXT, GL_RGBA}; uint32_t foundFormatIndex = swapchainFormatCount; for (auto&& desiredFormat : formats) { foundFormatIndex = swapchainFormatCount; for (uint32_t i = 0; i < swapchainFormatCount; i++) { if (swapchainFormats[i] == desiredFormat) { if (printAll) { Log::D("found desired framebuffer format 0x%x!\n", desiredFormat); } foundFormatIndex = i; break; } } if (foundFormatIndex != swapchainFormatCount) { break; } } int64_t format; if (foundFormatIndex == swapchainFormatCount) { Log::W("could not find any desired swapchain format!\n"); format = swapchainFormats[0]; } else { format = swapchainFormats[foundFormatIndex]; } std::vector swapchainLengths(viewConfigs.size()); swapchains.resize(viewConfigs.size()); for (uint32_t i = 0; i < viewConfigs.size(); i++) { XrSwapchainCreateInfo sci = {}; sci.type = XR_TYPE_SWAPCHAIN_CREATE_INFO; sci.next = NULL; sci.createFlags = 0; sci.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT; sci.format = format; sci.sampleCount = 1; sci.width = viewConfigs[i].recommendedImageRectWidth; sci.height = viewConfigs[i].recommendedImageRectHeight; sci.faceCount = 1; sci.arraySize = 1; sci.mipCount = 1; XrSwapchain swapchainHandle = XR_NULL_HANDLE; result = xrCreateSwapchain(session, &sci, &swapchainHandle); if (!CheckResult(instance, result, "xrCreateSwapchain")) { return false; } swapchains[i].handle = swapchainHandle; swapchains[i].width = sci.width; swapchains[i].height = sci.height; uint32_t length; result = xrEnumerateSwapchainImages(swapchains[i].handle, 0, &length, nullptr); if (!CheckResult(instance, result, "xrEnumerateSwapchainImages")) { return false; } swapchainLengths[i] = length; } swapchainImages.resize(viewConfigs.size()); for (uint32_t i = 0; i < viewConfigs.size(); i++) { swapchainImages[i].resize(swapchainLengths[i]); for (uint32_t j = 0; j < swapchainLengths[i]; j++) { #if defined(XR_USE_GRAPHICS_API_OPENGL) swapchainImages[i][j].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR; #elif defined(XR_USE_GRAPHICS_API_OPENGL_ES) swapchainImages[i][j].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR; #endif swapchainImages[i][j].next = NULL; } result = xrEnumerateSwapchainImages(swapchains[i].handle, swapchainLengths[i], &swapchainLengths[i], (XrSwapchainImageBaseHeader*)(swapchainImages[i].data())); if (!CheckResult(instance, result, "xrEnumerateSwapchainImages")) { return false; } } return true; } static GLuint CreateDepthTexture(GLuint colorTexture, GLint width, GLint height) { glBindTexture(GL_TEXTURE_2D, colorTexture); uint32_t depthTexture; glGenTextures(1, &depthTexture); glBindTexture(GL_TEXTURE_2D, depthTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); return depthTexture; } XrBuddy::XrBuddy(MainContext& mainContextIn, const glm::vec2& nearFarIn): mainContext(mainContextIn) { nearFar = nearFarIn; #ifdef XR_USE_GRAPHICS_API_OPENGL std::vector requiredExtensionVec = {XR_KHR_OPENGL_ENABLE_EXTENSION_NAME}; #elif defined(XR_USE_GRAPHICS_API_OPENGL_ES) std::vector requiredExtensionVec = {XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME}; #endif std::vector optionalExtensionVec = {XR_FB_COLOR_SPACE_EXTENSION_NAME}; std::vector extensionVec; #ifdef __ANDROID__ PFN_xrInitializeLoaderKHR xrInitializeLoaderKHR; xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR", (PFN_xrVoidFunction*)&xrInitializeLoaderKHR); if (xrInitializeLoaderKHR != NULL) { XrLoaderInitInfoAndroidKHR loaderInitializeInfoAndroid = {}; loaderInitializeInfoAndroid.type = XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR; loaderInitializeInfoAndroid.applicationVM = mainContext.androidApp->activity->vm; loaderInitializeInfoAndroid.applicationContext = mainContext.androidApp->activity->clazz; xrInitializeLoaderKHR((XrLoaderInitInfoBaseHeaderKHR*)&loaderInitializeInfoAndroid); } #endif if (!EnumerateExtensions(extensionProps)) { return; } for (auto&& ext : requiredExtensionVec) { if (!ExtensionSupported(extensionProps, ext)) { Log::W("required extension \"%s\" not supported!\n", ext); return; } extensionVec.push_back(ext); } for (auto&& ext : optionalExtensionVec) { if (ExtensionSupported(extensionProps, ext)) { extensionVec.push_back(ext); } } if (!EnumerateApiLayers(layerProps)) { return; } if (!CreateInstance(instance, extensionVec)) { return; } if (!GetSystemId(instance, systemId)) { return; } if (!GetSystemProperties(instance, systemId, systemProperties)) { return; } if (!SupportsVR(instance, systemId)) { Log::E("System doesn't support VR\n"); return; } if (!EnumerateViewConfigs(instance, systemId, viewConfigs)) { return; } constructorSucceded = true; } bool XrBuddy::Init() { if (!constructorSucceded) { return false; } if (!CreateSession(instance, systemId, session, mainContext)) { return false; } if (ExtensionSupported(extensionProps, XR_FB_COLOR_SPACE_EXTENSION_NAME)) { SetColorSpace(instance, session, XR_COLOR_SPACE_REC709_FB); } if (!CreateActions(instance, systemId, session, actionSet, actionMap)) { return false; } if (!CreateSpaces(instance, systemId, session, stageSpace, viewSpace, viewSpaceLocation, viewSpaceVelocity)) { return false; } if (!CreateFrameBuffer(frameBuffer)) { return false; } if (!CreateSwapchains(instance, session, viewConfigs, swapchains, swapchainImages)) { return false; } return true; } bool XrBuddy::PollEvents() { ZoneScoped; XrEventDataBuffer xrEvent = {}; xrEvent.type = XR_TYPE_EVENT_DATA_BUFFER; xrEvent.next = NULL; while (xrPollEvent(instance, &xrEvent) == XR_SUCCESS) { switch (xrEvent.type) { case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: // Receiving the XrEventDataInstanceLossPending event structure indicates that the application is about to lose the indicated XrInstance at the indicated lossTime in the future. // The application should call xrDestroyInstance and relinquish any instance-specific resources. // This typically occurs to make way for a replacement of the underlying runtime, such as via a software update. Log::D("xrEvent: XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING\n"); break; case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: { // Receiving the XrEventDataSessionStateChanged event structure indicates that the application has changed lifecycle stat.e Log::D("xrEvent: XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED -> "); XrEventDataSessionStateChanged* ssc = (XrEventDataSessionStateChanged*)&xrEvent; state = ssc->state; switch (state) { case XR_SESSION_STATE_IDLE: // The initial state after calling xrCreateSession or returned to after calling xrEndSession. Log::D("XR_SESSION_STATE_IDLE\n"); break; case XR_SESSION_STATE_READY: // The application is ready to call xrBeginSession and sync its frame loop with the runtime. Log::D("XR_SESSION_STATE_READY\n"); if (!BeginSession(instance, systemId, session)) { return false; } sessionReady = true; break; case XR_SESSION_STATE_SYNCHRONIZED: // The application has synced its frame loop with the runtime but is not visible to the user. Log::D("XR_SESSION_STATE_SYNCHRONIZED\n"); break; case XR_SESSION_STATE_VISIBLE: // The application has synced its frame loop with the runtime and is visible to the user but cannot receive XR input. Log::D("XR_SESSION_STATE_VISIBLE\n"); break; case XR_SESSION_STATE_FOCUSED: // The application has synced its frame loop with the runtime, is visible to the user and can receive XR input. Log::D("XR_SESSION_STATE_FOCUSED\n"); break; case XR_SESSION_STATE_STOPPING: Log::D("XR_SESSION_STATE_STOPPING\n"); // The application should exit its frame loop and call xrEndSession. if (!EndSession(instance, systemId, session)) { return false; } sessionReady = false; break; case XR_SESSION_STATE_LOSS_PENDING: Log::D("XR_SESSION_STATE_LOSS_PENDING\n"); // The session is in the process of being lost. The application should destroy the current session and can optionally recreate it. break; case XR_SESSION_STATE_EXITING: Log::D("XR_SESSION_STATE_EXITING\n"); // The application should end its XR experience and not automatically restart it. break; default: Log::D("XR_SESSION_STATE_??? %d\n", (int)state); break; } break; } case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING: // The XrEventDataReferenceSpaceChangePending event is sent to the application to notify it that the origin (and perhaps the bounds) of a reference space is changing. Log::D("XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING\n"); break; case XR_TYPE_EVENT_DATA_EVENTS_LOST: // Receiving the XrEventDataEventsLost event structure indicates that the event queue overflowed and some events were removed at the position within the queue at which this event was found. Log::D("xrEvent: XR_TYPE_EVENT_DATA_EVENTS_LOST\n"); break; case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED: // The XrEventDataInteractionProfileChanged event is sent to the application to notify it that the active input form factor for one or more top level user paths has changed.: Log::D("XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED\n"); break; default: Log::D("Unhandled event type %d\n", xrEvent.type); break; } xrEvent.type = XR_TYPE_EVENT_DATA_BUFFER; xrEvent.next = NULL; } return true; } bool XrBuddy::SyncInput() { ZoneScoped; if (state == XR_SESSION_STATE_FOCUSED) { XrResult result; XrActiveActionSet aas = {}; aas.actionSet = actionSet; aas.subactionPath = XR_NULL_PATH; XrActionsSyncInfo asi = {}; asi.type = XR_TYPE_ACTIONS_SYNC_INFO; asi.next = NULL; asi.countActiveActionSets = 1; asi.activeActionSets = &aas; result = xrSyncActions(session, &asi); if (!CheckResult(instance, result, "xrSyncActions")) { return false; } bool printActions = false; for (auto& iter : actionMap) { ActionInfo& actionInfo = iter.second; XrActionStateGetInfo getInfo = {}; getInfo.type = XR_TYPE_ACTION_STATE_GET_INFO; getInfo.next = NULL; getInfo.action = actionInfo.action; getInfo.subactionPath = 0; iter.second.u.boolState.next = NULL; switch (iter.second.type) { case XR_ACTION_TYPE_BOOLEAN_INPUT: iter.second.u.boolState.type = XR_TYPE_ACTION_STATE_BOOLEAN; result = xrGetActionStateBoolean(session, &getInfo, &iter.second.u.boolState); if (!CheckResult(instance, result, "xrGetActionStateBoolean")) { return false; } if (printActions && iter.second.u.boolState.changedSinceLastSync) { Log::D("action %s:\n", iter.first.c_str()); Log::D(" currentState: %s\n", iter.second.u.boolState.currentState ? "true" : "false"); Log::D(" changedSinceLastSync: %s\n", iter.second.u.boolState.changedSinceLastSync ? "true" : "false"); Log::D(" lastChangeTime: %lld\n", iter.second.u.boolState.lastChangeTime); Log::D(" isActive: %s\n", iter.second.u.boolState.isActive ? "true" : "false"); } break; case XR_ACTION_TYPE_FLOAT_INPUT: iter.second.u.floatState.type = XR_TYPE_ACTION_STATE_FLOAT; result = xrGetActionStateFloat(session, &getInfo, &iter.second.u.floatState); if (!CheckResult(instance, result, "xrGetActionStateFloat")) { return false; } if (printActions && iter.second.u.floatState.changedSinceLastSync) { Log::D("action %s:\n", iter.first.c_str()); Log::D(" currentState: %.5f\n", iter.second.u.floatState.currentState); Log::D(" changedSinceLastSync: %s\n", iter.second.u.floatState.changedSinceLastSync ? "true" : "false"); Log::D(" lastChangeTime: %lld\n", iter.second.u.floatState.lastChangeTime); Log::D(" isActive: %s\n", iter.second.u.floatState.isActive ? "true" : "false"); } break; case XR_ACTION_TYPE_VECTOR2F_INPUT: iter.second.u.vec2State.type = XR_TYPE_ACTION_STATE_VECTOR2F; result = xrGetActionStateVector2f(session, &getInfo, &iter.second.u.vec2State); if (!CheckResult(instance, result, "xrGetActionStateVector2f")) { return false; } if (printActions && iter.second.u.vec2State.changedSinceLastSync) { Log::D("action %s:\n", iter.first.c_str()); Log::D(" currentState: (%.5f, %.5f)\n", iter.second.u.vec2State.currentState.x, iter.second.u.vec2State.currentState.y); Log::D(" changedSinceLastSync: %s\n", iter.second.u.vec2State.changedSinceLastSync ? "true" : "false"); Log::D(" lastChangeTime: %lld\n", iter.second.u.vec2State.lastChangeTime); Log::D(" isActive: %s\n", iter.second.u.vec2State.isActive ? "true" : "false"); } break; case XR_ACTION_TYPE_POSE_INPUT: iter.second.u.poseState.type = XR_TYPE_ACTION_STATE_POSE; result = xrGetActionStatePose(session, &getInfo, &iter.second.u.poseState); if (!CheckResult(instance, result, "xrGetActionStatePose")) { return false; } break; default: break; } } } return true; } bool XrBuddy::GetActionBool(const std::string& actionName, bool* value, bool* valid, bool* changed) const { auto iter = actionMap.find(actionName); if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_BOOLEAN_INPUT) { return false; } if (state != XR_SESSION_STATE_FOCUSED) { *value = false; *valid = false; *changed = false; return true; } *value = iter->second.u.boolState.currentState; *valid = iter->second.u.boolState.isActive; *changed = iter->second.u.boolState.changedSinceLastSync; return true; } bool XrBuddy::GetActionFloat(const std::string& actionName, float* value, bool* valid, bool* changed) const { auto iter = actionMap.find(actionName); if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_FLOAT_INPUT) { return false; } if (state != XR_SESSION_STATE_FOCUSED) { *value = 0.0f; *valid = false; *changed = false; return true; } *value = iter->second.u.floatState.currentState; *valid = iter->second.u.floatState.isActive; *changed = iter->second.u.floatState.changedSinceLastSync; return true; } bool XrBuddy::GetActionVec2(const std::string& actionName, glm::vec2* value, bool* valid, bool* changed) const { auto iter = actionMap.find(actionName); if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_VECTOR2F_INPUT) { return false; } if (state != XR_SESSION_STATE_FOCUSED) { *value = glm::vec2(0.0f); *valid = false; *changed = false; return true; } *value = glm::vec2(iter->second.u.vec2State.currentState.x, iter->second.u.vec2State.currentState.y); *valid = iter->second.u.vec2State.isActive; *changed = iter->second.u.vec2State.changedSinceLastSync; return true; } bool XrBuddy::GetActionPosition(const std::string& actionName, glm::vec3* value, bool* valid, bool* tracked) const { // special case for "head_pose" if (actionName == "head_pose") { *value = glm::vec3(viewSpaceLocation.pose.position.x, viewSpaceLocation.pose.position.y, viewSpaceLocation.pose.position.z); *valid = viewSpaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT; *tracked = viewSpaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT; return true; } auto iter = actionMap.find(actionName); if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_POSE_INPUT) { return false; } if (state != XR_SESSION_STATE_FOCUSED || !iter->second.u.poseState.isActive) { *value = glm::vec3(0.0f); *valid = false; *tracked = false; return true; } *value = glm::vec3(iter->second.spaceLocation.pose.position.x, iter->second.spaceLocation.pose.position.y, iter->second.spaceLocation.pose.position.z); *valid = iter->second.spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT; *tracked = iter->second.spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT; return true; } bool XrBuddy::GetActionOrientation(const std::string& actionName, glm::quat* value, bool* valid, bool* tracked) const { // special case for "head_pose" if (actionName == "head_pose") { *value = glm::quat(viewSpaceLocation.pose.orientation.w, viewSpaceLocation.pose.orientation.x, viewSpaceLocation.pose.orientation.y, viewSpaceLocation.pose.orientation.z); *valid = viewSpaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT; *tracked = viewSpaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT; return true; } auto iter = actionMap.find(actionName); if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_POSE_INPUT) { return false; } if (state != XR_SESSION_STATE_FOCUSED || !iter->second.u.poseState.isActive) { *value = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); *valid = false; *tracked = false; return true; } *value = glm::quat(iter->second.spaceLocation.pose.orientation.w, iter->second.spaceLocation.pose.orientation.x, iter->second.spaceLocation.pose.orientation.y, iter->second.spaceLocation.pose.orientation.z); *valid = iter->second.spaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT; *tracked = iter->second.spaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT; return true; } bool XrBuddy::GetActionLinearVelocity(const std::string& actionName, glm::vec3* value, bool* valid) const { // special case for "head_pose" if (actionName == "head_pose") { *value = glm::vec3(viewSpaceVelocity.linearVelocity.x, viewSpaceVelocity.linearVelocity.y, viewSpaceVelocity.linearVelocity.z); *valid = viewSpaceVelocity.velocityFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT; return true; } auto iter = actionMap.find(actionName); if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_POSE_INPUT) { return false; } if (state != XR_SESSION_STATE_FOCUSED || !iter->second.u.poseState.isActive) { *value = glm::vec3(0.0f); *valid = false; return true; } *value = glm::vec3(iter->second.spaceVelocity.linearVelocity.x, iter->second.spaceVelocity.linearVelocity.y, iter->second.spaceVelocity.linearVelocity.z); *valid = iter->second.spaceVelocity.velocityFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT; return true; } bool XrBuddy::GetActionAngularVelocity(const std::string& actionName, glm::vec3* value, bool* valid) const { // special case for "head_pose" if (actionName == "head_pose") { *value = glm::vec3(viewSpaceVelocity.angularVelocity.x, viewSpaceVelocity.angularVelocity.y, viewSpaceVelocity.angularVelocity.z); *valid = viewSpaceVelocity.velocityFlags & XR_SPACE_VELOCITY_ANGULAR_VALID_BIT; return true; } auto iter = actionMap.find(actionName); if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_POSE_INPUT) { return false; } if (state != XR_SESSION_STATE_FOCUSED || !iter->second.u.poseState.isActive) { *value = glm::vec3(0.0f); *valid = false; return true; } *value = glm::vec3(iter->second.spaceVelocity.angularVelocity.x, iter->second.spaceVelocity.angularVelocity.y, iter->second.spaceVelocity.angularVelocity.z); *valid = iter->second.spaceVelocity.velocityFlags & XR_SPACE_VELOCITY_ANGULAR_VALID_BIT; return true; } uint32_t XrBuddy::GetColorTexture() const { return prevLastColorTexture; } void XrBuddy::CycleColorSpace() { static int i = 0; Log::D("SETTING COLOR SPACE -> %d\n", i); SetColorSpace(instance, session, (XrColorSpaceFB)i); i = (i + 1) % (XR_COLOR_SPACE_ADOBE_RGB_FB + 1); } bool XrBuddy::LocateSpaces(XrTime predictedDisplayTime) { ZoneScoped; viewSpaceLocation = {}; viewSpaceVelocity = {}; viewSpaceLocation.type = XR_TYPE_SPACE_LOCATION; viewSpaceLocation.next = &viewSpaceVelocity; viewSpaceVelocity.type = XR_TYPE_SPACE_VELOCITY; viewSpaceVelocity.next = NULL; XrResult result; if (state == XR_SESSION_STATE_FOCUSED) { for (auto& iter : actionMap) { if (iter.second.type == XR_ACTION_TYPE_POSE_INPUT && iter.second.u.poseState.isActive) { // link location -> velocity iter.second.spaceLocation.next = &iter.second.spaceVelocity; result = xrLocateSpace(iter.second.space, stageSpace, predictedDisplayTime, &iter.second.spaceLocation); if (!CheckResult(instance, result, "xrLocateSpace")) { return false; } } } result = xrLocateSpace(viewSpace, stageSpace, predictedDisplayTime, &viewSpaceLocation); if (!CheckResult(instance, result, "xrLocateSpace")) { return false; } } return true; } bool XrBuddy::SessionReady() const { return sessionReady; } bool XrBuddy::RenderFrame() { ZoneScoped; if (state == XR_SESSION_STATE_READY || state == XR_SESSION_STATE_SYNCHRONIZED || state == XR_SESSION_STATE_VISIBLE || state == XR_SESSION_STATE_FOCUSED) { XrFrameState fs = {}; fs.type = XR_TYPE_FRAME_STATE; fs.next = NULL; XrFrameWaitInfo fwi = {}; fwi.type = XR_TYPE_FRAME_WAIT_INFO; fwi.next = NULL; XrResult result; { ZoneScopedNC("xrWaitFrame", tracy::Color::Red4); result = xrWaitFrame(session, &fwi, &fs); if (!CheckResult(instance, result, "xrWaitFrame")) { return false; } } lastPredictedDisplayTime = fs.predictedDisplayTime; XrFrameBeginInfo fbi = {}; fbi.type = XR_TYPE_FRAME_BEGIN_INFO; fbi.next = NULL; { ZoneScopedNC("xrBeginFrame", tracy::Color::DarkGreen); result = xrBeginFrame(session, &fbi); if (!CheckResult(instance, result, "xrBeginFrame")) { return false; } } std::vector layers; XrCompositionLayerProjection layer = {}; layer.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION; layer.next = NULL; std::vector projectionLayerViews; if (fs.shouldRender == XR_TRUE) { if (!LocateSpaces(fs.predictedDisplayTime)) { return false; } if (RenderLayer(fs.predictedDisplayTime, projectionLayerViews, layer)) { layers.push_back(reinterpret_cast(&layer)); } } XrFrameEndInfo fei = {}; fei.type = XR_TYPE_FRAME_END_INFO; fei.next = NULL; fei.displayTime = fs.predictedDisplayTime; fei.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; fei.layerCount = (uint32_t)layers.size(); fei.layers = layers.data(); { ZoneScopedNC("xrEndFrame", tracy::Color::Red4); result = xrEndFrame(session, &fei); if (!CheckResult(instance, result, "xrEndFrame")) { return false; } } } return true; } bool XrBuddy::Shutdown() { for (auto& swapchain : swapchains) { xrDestroySwapchain(swapchain.handle); } swapchains.clear(); swapchainImages.clear(); for (auto& iter : colorToDepthMap) { glDeleteTextures(1, &iter.second); } colorToDepthMap.clear(); if (frameBuffer) { glDeleteFramebuffers(1, &frameBuffer); frameBuffer = 0; } if (stageSpace != XR_NULL_HANDLE) { xrDestroySpace(stageSpace); stageSpace = XR_NULL_HANDLE; } if (viewSpace != XR_NULL_HANDLE) { xrDestroySpace(viewSpace); viewSpace = XR_NULL_HANDLE; } for (auto iter : actionMap) { if (iter.second.space != XR_NULL_HANDLE) { xrDestroySpace(iter.second.space); } if (iter.second.action != XR_NULL_HANDLE) { xrDestroyAction(iter.second.action); } } actionMap.clear(); if (actionSet != XR_NULL_HANDLE) { xrDestroyActionSet(actionSet); actionSet = XR_NULL_HANDLE; } if (session != XR_NULL_HANDLE) { xrDestroySession(session); session = XR_NULL_HANDLE; } // on oculus runtime destroying the instance causes a crash on shutdown... so don't... /* if (instance != XR_NULL_HANDLE) { xrDestroyInstance(instance); instance = XR_NULL_HANDLE; } */ return true; } bool XrBuddy::RenderLayer(XrTime predictedDisplayTime, std::vector& projectionLayerViews, XrCompositionLayerProjection& layer) { ZoneScoped; XrViewState viewState = {}; viewState.type = XR_TYPE_VIEW_STATE; viewState.next = NULL; uint32_t viewCapacityInput = (uint32_t)viewConfigs.size(); uint32_t viewCountOutput; std::vector views(viewConfigs.size()); for (size_t i = 0; i < viewConfigs.size(); i++) { views[i] = {}; views[i].type = XR_TYPE_VIEW; views[i].next = NULL; } XrViewLocateInfo vli = {}; vli.type = XR_TYPE_VIEW_LOCATE_INFO; vli.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; vli.displayTime = predictedDisplayTime; vli.space = stageSpace; vli.next = NULL; XrResult result = xrLocateViews(session, &vli, &viewState, viewCapacityInput, &viewCountOutput, views.data()); if (!CheckResult(instance, result, "xrLocateViews")) { return false; } if (XR_UNQUALIFIED_SUCCESS(result)) { assert(viewCountOutput == viewCapacityInput); assert(viewCountOutput == viewConfigs.size()); assert(viewCountOutput == swapchains.size()); projectionLayerViews.resize(viewCountOutput); // Render view to the appropriate part of the swapchain image. for (uint32_t i = 0; i < viewCountOutput; i++) { // Each view has a separate swapchain which is acquired, rendered to, and released. const SwapchainInfo& viewSwapchain = swapchains[i]; XrSwapchainImageAcquireInfo ai = {}; ai.type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO; ai.next = NULL; uint32_t swapchainImageIndex; result = xrAcquireSwapchainImage(viewSwapchain.handle, &ai, &swapchainImageIndex); if (!CheckResult(instance, result, "xrAcquireSwapchainImage")) { return false; } XrSwapchainImageWaitInfo wi = {}; wi.type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO; wi.next = NULL; wi.timeout = XR_INFINITE_DURATION; result = xrWaitSwapchainImage(viewSwapchain.handle, &wi); if (!CheckResult(instance, result, "xrWaitSwapchainImage")) { return false; } projectionLayerViews[i] = {}; projectionLayerViews[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; projectionLayerViews[i].pose = views[i].pose; projectionLayerViews[i].fov = views[i].fov; projectionLayerViews[i].subImage.swapchain = viewSwapchain.handle; projectionLayerViews[i].subImage.imageRect.offset = {0, 0}; projectionLayerViews[i].subImage.imageRect.extent = {viewSwapchain.width, viewSwapchain.height}; const SwapchainImage& swapchainImage = swapchainImages[i][swapchainImageIndex]; // find or create the depthTexture associated with this colorTexture const uint32_t colorTexture = swapchainImage.image; if (i == 0) { prevLastColorTexture = lastColorTexture; lastColorTexture = colorTexture; // save for rendering onto desktop. } auto iter = colorToDepthMap.find(colorTexture); if (iter == colorToDepthMap.end()) { const uint32_t depthTexture = CreateDepthTexture(colorTexture, viewSwapchain.width, viewSwapchain.height); iter = colorToDepthMap.insert(std::make_pair(colorTexture, depthTexture)).first; } RenderView(projectionLayerViews[i], frameBuffer, iter->first, iter->second, i); XrSwapchainImageReleaseInfo ri = {}; ri.type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO; ri.next = NULL; result = xrReleaseSwapchainImage(viewSwapchain.handle, &ri); if (!CheckResult(instance, result, "xrReleaseSwapchainImage")) { return false; } layer.space = stageSpace; layer.viewCount = (uint32_t)projectionLayerViews.size(); layer.views = projectionLayerViews.data(); } } return true; } void XrBuddy::RenderView(const XrCompositionLayerProjectionView& layerView, uint32_t frameBuffer, uint32_t colorTexture, uint32_t depthTexture, int32_t viewNum) { glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer); glViewport(static_cast(layerView.subImage.imageRect.offset.x), static_cast(layerView.subImage.imageRect.offset.y), static_cast(layerView.subImage.imageRect.extent.width), static_cast(layerView.subImage.imageRect.extent.height)); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTexture, 0); const float tanLeft = tanf(layerView.fov.angleLeft); const float tanRight = tanf(layerView.fov.angleRight); const float tanDown = tanf(layerView.fov.angleDown); const float tanUp = tanf(layerView.fov.angleUp); glm::mat4 projMat; CreateProjection(glm::value_ptr(projMat), GRAPHICS_OPENGL, tanLeft, tanRight, tanUp, tanDown, nearFar.x, nearFar.y); const auto& pose = layerView.pose; glm::quat eyeRot(pose.orientation.w, pose.orientation.x, pose.orientation.y, pose.orientation.z); glm::vec3 eyePos(pose.position.x, pose.position.y, pose.position.z); glm::mat4 eyeMat = MakeMat4(eyeRot, eyePos); glm::vec4 viewport(layerView.subImage.imageRect.offset.x, layerView.subImage.imageRect.offset.y, layerView.subImage.imageRect.extent.width, layerView.subImage.imageRect.extent.height); renderCallback(projMat, eyeMat, viewport, nearFar, viewNum); glBindFramebuffer(GL_FRAMEBUFFER, 0); } ================================================ FILE: src/core/xrbuddy.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include #if defined(WIN32) #define XR_USE_PLATFORM_WIN32 1 #define XR_USE_GRAPHICS_API_OPENGL 1 #include #elif defined(__ANDROID__) #define XR_USE_PLATFORM_ANDROID 1 #define XR_USE_GRAPHICS_API_OPENGL_ES 1 #include #include #include #include #include #include #elif defined(__linux__) #define XR_USE_PLATFORM_XLIB 1 #define XR_USE_GRAPHICS_API_OPENGL 1 #include #include #include // for fullscreen video mode #include // for resolution changes #include #include // Conficts with core/optionparser.h #ifdef None #undef None #endif #endif #include #include #include #include "maincontext.h" class XrBuddy { public: XrBuddy(MainContext& mainContextIn, const glm::vec2& nearFarIn); bool Init(); bool PollEvents(); bool SyncInput(); using RenderCallback = std::function; void SetRenderCallback(RenderCallback renderCallbackIn) { renderCallback = renderCallbackIn; } bool SessionReady() const; bool RenderFrame(); bool Shutdown(); struct SwapchainInfo { XrSwapchain handle; int32_t width; int32_t height; }; struct ActionInfo { ActionInfo() {} ActionInfo(XrAction actionIn, XrActionType typeIn, XrSpace spaceIn) : action(actionIn), type(typeIn), space(spaceIn) { spaceLocation.type = XR_TYPE_SPACE_LOCATION; spaceLocation.next = NULL; spaceVelocity.type = XR_TYPE_SPACE_VELOCITY; spaceVelocity.next = NULL; } XrAction action; XrActionType type; union { XrActionStateBoolean boolState; XrActionStateFloat floatState; XrActionStateVector2f vec2State; XrActionStatePose poseState; } u; XrSpace space; XrSpaceLocation spaceLocation; XrSpaceVelocity spaceVelocity; }; bool GetActionBool(const std::string& actionName, bool* value, bool* valid, bool* changed) const; bool GetActionFloat(const std::string& actionName, float* value, bool* valid, bool* changed) const; bool GetActionVec2(const std::string& actionName, glm::vec2* value, bool* valid, bool* changed) const; bool GetActionPosition(const std::string& actionName, glm::vec3* value, bool* valid, bool* tracked) const; bool GetActionOrientation(const std::string& actionName, glm::quat* value, bool* valid, bool* tracked) const; bool GetActionLinearVelocity(const std::string& actionName, glm::vec3* value, bool* valid) const; bool GetActionAngularVelocity(const std::string& actionName, glm::vec3* value, bool* valid) const; uint32_t GetColorTexture() const; void CycleColorSpace(); #if defined(XR_USE_GRAPHICS_API_OPENGL) using SwapchainImage = XrSwapchainImageOpenGLKHR; #elif defined(XR_USE_GRAPHICS_API_OPENGL_ES) using SwapchainImage = XrSwapchainImageOpenGLESKHR; #endif protected: bool LocateSpaces(XrTime predictedDisplayTime); bool RenderLayer(XrTime predictedDisplayTime, std::vector& projectionLayerViews, XrCompositionLayerProjection& layer); void RenderView(const XrCompositionLayerProjectionView& layerView, uint32_t frameBuffer, uint32_t colorTexture, uint32_t depthTexture, int32_t viewNum); bool constructorSucceded = false; MainContext& mainContext; XrSessionState state = XR_SESSION_STATE_UNKNOWN; std::vector extensionProps; std::vector layerProps; std::vector viewConfigs; XrInstance instance = XR_NULL_HANDLE; XrSystemId systemId = XR_NULL_SYSTEM_ID; XrSystemProperties systemProperties = {}; XrSession session = XR_NULL_HANDLE; XrActionSet actionSet = XR_NULL_HANDLE; std::map actionMap; XrSpace stageSpace = XR_NULL_HANDLE; XrSpace viewSpace = XR_NULL_HANDLE; XrSpaceLocation viewSpaceLocation = {}; XrSpaceVelocity viewSpaceVelocity = {}; std::vector swapchains; std::vector> swapchainImages; uint32_t frameBuffer = 0; std::map colorToDepthMap; XrTime lastPredictedDisplayTime = 0; uint32_t prevLastColorTexture = 0; uint32_t lastColorTexture = 0; bool sessionReady = false; RenderCallback renderCallback; glm::vec2 nearFar; }; ================================================ FILE: src/flycam.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "flycam.h" #include "core/log.h" #include "core/util.h" FlyCam::FlyCam(const glm::vec3& worldUpIn, const glm::vec3& posIn, const glm::quat& rotIn, float speedIn, float rotSpeedIn) : worldUp(worldUpIn), pos(posIn), vel(0.0f, 0.0f, 0.0f), rot(rotIn), cameraMat(MakeMat4(rot, pos)), speed(speedIn), rotSpeed(rotSpeedIn) { } void FlyCam::Process(const glm::vec2& leftStickIn, const glm::vec2& rightStickIn, float rollAmountIn, float upAmountIn, float dt) { glm::vec2 leftStick = leftStickIn; glm::vec2 rightStick = rightStickIn; float rollAmount = rollAmountIn; const float STIFF = 15.0f; const float K = STIFF / speed; // left stick controls position glm::vec3 stick = rot * glm::vec3(leftStick.x, upAmountIn, -leftStick.y); glm::vec3 s_over_k = (stick * STIFF) / K; glm::vec3 s_over_k_sq = (stick * STIFF) / (K * K); float e_neg_kt = exp(-K * dt); glm::vec3 v = s_over_k + e_neg_kt * (vel - s_over_k); pos = s_over_k * dt + (s_over_k_sq - vel / K) * e_neg_kt + pos - s_over_k_sq + (vel / K); vel = v; // right stick controls orientation //glm::vec3 up = rot * glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 right = rot * glm::vec3(1.0f, 0.0f, 0.0f); glm::vec3 forward = rot * glm::vec3(0.0f, 0.0f, -1.0f); glm::quat yaw = glm::angleAxis(rotSpeed * dt * -rightStick.x, worldUp); glm::quat pitch = glm::angleAxis(rotSpeed * dt * rightStick.y, right); rot = (yaw * pitch) * rot; // axes of new cameraMat glm::vec3 x = rot * glm::vec3(1.0f, 0.0f, 0.0f); glm::vec3 y = rot * glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 z = rot * glm::vec3(0.0f, 0.0f, 1.0f); // apply roll to worldUp if (fabs(rollAmountIn) > 0.1f) { worldUp = glm::vec3(cameraMat[1]); glm::quat roll = glm::angleAxis(rotSpeed * dt * rollAmount, forward); worldUp = roll * worldUp; } // make sure that cameraMat will be orthogonal, and aligned with world up. if (glm::dot(z, worldUp) < 0.999f) // if w are aren't looking stright up. { glm::vec3 xx = glm::normalize(glm::cross(worldUp, z)); glm::vec3 yy = glm::normalize(glm::cross(z, xx)); cameraMat = glm::mat4(glm::vec4(xx, 0.0f), glm::vec4(yy, 0.0f), glm::vec4(z, 0.0f), glm::vec4(pos, 1.0f)); } else { cameraMat = glm::mat4(glm::vec4(x, 0.0f), glm::vec4(y, 0.0f), glm::vec4(z, 0.0f), glm::vec4(pos, 1.0f)); } glm::vec3 unusedScale; Decompose(cameraMat, &unusedScale, &rot); } void FlyCam::SetCameraMat(const glm::mat4& cameraMat) { pos = glm::vec3(cameraMat[3]); rot = glm::normalize(glm::quat(glm::mat3(cameraMat))); vel = glm::vec3(0.0f, 0.0f, 0.0f); } ================================================ FILE: src/flycam.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include class FlyCam { public: FlyCam(const glm::vec3& worldUpIn, const glm::vec3& posIn, const glm::quat& rotIn, float speedIn, float rotSpeedIn); void Process(const glm::vec2& leftStickIn, const glm::vec2& rightStickIn, float rollAmountIn, float upAmountIn, float dt); const glm::mat4& GetCameraMat() const { return cameraMat; } void SetCameraMat(const glm::mat4& cameraMat); protected: float speed; // units per sec float rotSpeed; // radians per sec glm::vec3 worldUp; glm::vec3 pos; glm::vec3 vel; glm::quat rot; glm::mat4 cameraMat; }; ================================================ FILE: src/gaussiancloud.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "gaussiancloud.h" #include #include #include #include #include #include #include #include #include #ifdef TRACY_ENABLE #include #else #define ZoneScoped #define ZoneScopedNC(NAME, COLOR) #endif #include "core/log.h" #include "core/util.h" #include "ply.h" struct BaseGaussianData { BaseGaussianData() noexcept {} float posWithAlpha[4]; // center of the gaussian in object coordinates, with alpha in w float r_sh0[4]; // sh coeff for red channel (up to third-order) float g_sh0[4]; // sh coeff for green channel float b_sh0[4]; // sh coeff for blue channel float cov3_col0[3]; // 3x3 covariance matrix of the splat in object coordinates. float cov3_col1[3]; float cov3_col2[3]; }; struct FullGaussianData : public BaseGaussianData { FullGaussianData() noexcept {} float r_sh1[4]; float r_sh2[4]; float r_sh3[4]; float g_sh1[4]; float g_sh2[4]; float g_sh3[4]; float b_sh1[4]; float b_sh2[4]; float b_sh3[4]; }; // Function to convert glm::mat3 to Eigen::Matrix3f static Eigen::Matrix3f glmToEigen(const glm::mat3& glmMat) { Eigen::Matrix3f eigenMat; for (int r = 0; r < 3; ++r) { for (int c = 0; c < 3; ++c) { eigenMat(r, c) = glmMat[c][r]; } } return eigenMat; } // Function to convert Eigen::Matrix3f to glm::mat3 static glm::mat3 eigenToGlm(const Eigen::Matrix3f& eigenMat) { glm::mat3 glmMat; for (int r = 0; r < 3; ++r) { for (int c = 0; c < 3; ++c) { glmMat[c][r] = eigenMat(r, c); } } return glmMat; } static glm::mat3 ComputeCovMatFromRotScale(float rot[4], float scale[3]) { glm::quat q(rot[0], rot[1], rot[2], rot[3]); glm::mat3 R(glm::normalize(q)); glm::mat3 S(glm::vec3(scale[0], 0.0f, 0.0f), glm::vec3(0.0f, scale[1], 0.0f), glm::vec3(0.0f, 0.0f, scale[2])); return R * S * glm::transpose(S) * glm::transpose(R); } static void ComputeRotScaleFromCovMat(const glm::mat3& V, glm::quat& rotOut, glm::vec3& scaleOut) { Eigen::Matrix3f eigenV = glmToEigen(V); // Perform Eigen decomposition Eigen::SelfAdjointEigenSolver solver(eigenV); // Get eigenvectors and eigenvalues Eigen::Matrix3f eigenVec = solver.eigenvectors(); Eigen::Array3f eigenVal = solver.eigenvalues(); glm::mat3 R = eigenToGlm(eigenVec); // glm mat3 to quat only works when det is 1. if (glm::determinant(R) < 0) { R[0] *= -1.0f; R[1] *= -1.0f; R[2] *= -1.0f; } rotOut = glm::normalize(glm::quat(R)); // The eigenVal gives us the diagonal of (S*S^T), so take the sqrt to give is S. scaleOut = glm::vec3(sqrtf(eigenVal(0)), sqrtf(eigenVal(1)), sqrtf(eigenVal(2))); } static float ComputeAlphaFromOpacity(float opacity) { return 1.0f / (1.0f + expf(-opacity)); } static float ComputeOpacityFromAlpha(float alpha) { return -logf((1.0f / alpha) - 1.0f); } GaussianCloud::GaussianCloud(const Options& options) : numGaussians(0), gaussianSize(0), opt(options), hasFullSH(false) { ; } bool GaussianCloud::ImportPly(const std::string& plyFilename) { ZoneScopedNC("GC::ImportPly", tracy::Color::Red4); std::ifstream plyFile(plyFilename, std::ios::binary); if (!plyFile.is_open()) { Log::E("failed to open %s\n", plyFilename.c_str()); return false; } Ply ply; { ZoneScopedNC("ply.Parse", tracy::Color::Blue); if (!ply.Parse(plyFile)) { Log::E("Error parsing ply file \"%s\"\n", plyFilename.c_str()); return false; } } struct { BinaryAttribute x, y, z; BinaryAttribute f_dc[3]; BinaryAttribute f_rest[45]; BinaryAttribute opacity; BinaryAttribute scale[3]; BinaryAttribute rot[4]; } props; { ZoneScopedNC("ply.GetProps", tracy::Color::Green); if (!ply.GetProperty("x", props.x) || !ply.GetProperty("y", props.y) || !ply.GetProperty("z", props.z)) { Log::E("Error parsing ply file \"%s\", missing position property\n", plyFilename.c_str()); } for (int i = 0; i < 3; i++) { if (!ply.GetProperty("f_dc_" + std::to_string(i), props.f_dc[i])) { Log::E("Error parsing ply file \"%s\", missing f_dc property\n", plyFilename.c_str()); } } if (opt.importFullSH) { hasFullSH = true; for (int i = 0; i < 45; i++) { if (!ply.GetProperty("f_rest_" + std::to_string(i), props.f_rest[i])) { // f_rest properties are optional Log::W("PLY file \"%s\", missing f_rest property\n", plyFilename.c_str()); hasFullSH = false; break; } } } else { hasFullSH = false; } if (!ply.GetProperty("opacity", props.opacity)) { Log::E("Error parsing ply file \"%s\", missing opacity property\n", plyFilename.c_str()); } for (int i = 0; i < 3; i++) { if (!ply.GetProperty("scale_" + std::to_string(i), props.scale[i])) { Log::E("Error parsing ply file \"%s\", missing scale property\n", plyFilename.c_str()); } } for (int i = 0; i < 4; i++) { if (!ply.GetProperty("rot_" + std::to_string(i), props.rot[i])) { Log::E("Error parsing ply file \"%s\", missing rot property\n", plyFilename.c_str()); } } } InitAttribs(); { ZoneScopedNC("alloc data", tracy::Color::Red4); numGaussians = ply.GetVertexCount(); if (hasFullSH) { gaussianSize = sizeof(FullGaussianData); FullGaussianData* fullPtr = new FullGaussianData[numGaussians]; data.reset(fullPtr); } else { gaussianSize = sizeof(BaseGaussianData); BaseGaussianData* basePtr = new BaseGaussianData[numGaussians]; data.reset(basePtr); } } { ZoneScopedNC("ply.ForEachVertex", tracy::Color::Blue); int i = 0; uint8_t* rawPtr = (uint8_t*)data.get(); ply.ForEachVertex([this, &rawPtr, &i, &props](const void* plyData, size_t size) { BaseGaussianData* basePtr = reinterpret_cast(rawPtr); basePtr->posWithAlpha[0] = props.x.Read(plyData); basePtr->posWithAlpha[1] = props.y.Read(plyData); basePtr->posWithAlpha[2] = props.z.Read(plyData); basePtr->posWithAlpha[3] = ComputeAlphaFromOpacity(props.opacity.Read(plyData)); if (hasFullSH) { FullGaussianData* fullPtr = reinterpret_cast(rawPtr); fullPtr->r_sh0[0] = props.f_dc[0].Read(plyData); fullPtr->r_sh0[1] = props.f_rest[0].Read(plyData); fullPtr->r_sh0[2] = props.f_rest[1].Read(plyData); fullPtr->r_sh0[3] = props.f_rest[2].Read(plyData); fullPtr->r_sh1[0] = props.f_rest[3].Read(plyData); fullPtr->r_sh1[1] = props.f_rest[4].Read(plyData); fullPtr->r_sh1[2] = props.f_rest[5].Read(plyData); fullPtr->r_sh1[3] = props.f_rest[6].Read(plyData); fullPtr->r_sh2[0] = props.f_rest[7].Read(plyData); fullPtr->r_sh2[1] = props.f_rest[8].Read(plyData); fullPtr->r_sh2[2] = props.f_rest[9].Read(plyData); fullPtr->r_sh2[3] = props.f_rest[10].Read(plyData); fullPtr->r_sh3[0] = props.f_rest[11].Read(plyData); fullPtr->r_sh3[1] = props.f_rest[12].Read(plyData); fullPtr->r_sh3[2] = props.f_rest[13].Read(plyData); fullPtr->r_sh3[3] = props.f_rest[14].Read(plyData); fullPtr->g_sh0[0] = props.f_dc[1].Read(plyData); fullPtr->g_sh0[1] = props.f_rest[15].Read(plyData); fullPtr->g_sh0[2] = props.f_rest[16].Read(plyData); fullPtr->g_sh0[3] = props.f_rest[17].Read(plyData); fullPtr->g_sh1[0] = props.f_rest[18].Read(plyData); fullPtr->g_sh1[1] = props.f_rest[19].Read(plyData); fullPtr->g_sh1[2] = props.f_rest[20].Read(plyData); fullPtr->g_sh1[3] = props.f_rest[21].Read(plyData); fullPtr->g_sh2[0] = props.f_rest[22].Read(plyData); fullPtr->g_sh2[1] = props.f_rest[23].Read(plyData); fullPtr->g_sh2[2] = props.f_rest[24].Read(plyData); fullPtr->g_sh2[3] = props.f_rest[25].Read(plyData); fullPtr->g_sh3[0] = props.f_rest[26].Read(plyData); fullPtr->g_sh3[1] = props.f_rest[27].Read(plyData); fullPtr->g_sh3[2] = props.f_rest[28].Read(plyData); fullPtr->g_sh3[3] = props.f_rest[29].Read(plyData); fullPtr->b_sh0[0] = props.f_dc[2].Read(plyData); fullPtr->b_sh0[1] = props.f_rest[30].Read(plyData); fullPtr->b_sh0[2] = props.f_rest[31].Read(plyData); fullPtr->b_sh0[3] = props.f_rest[32].Read(plyData); fullPtr->b_sh1[0] = props.f_rest[33].Read(plyData); fullPtr->b_sh1[1] = props.f_rest[34].Read(plyData); fullPtr->b_sh1[2] = props.f_rest[35].Read(plyData); fullPtr->b_sh1[3] = props.f_rest[36].Read(plyData); fullPtr->b_sh2[0] = props.f_rest[37].Read(plyData); fullPtr->b_sh2[1] = props.f_rest[38].Read(plyData); fullPtr->b_sh2[2] = props.f_rest[39].Read(plyData); fullPtr->b_sh2[3] = props.f_rest[40].Read(plyData); fullPtr->b_sh3[0] = props.f_rest[41].Read(plyData); fullPtr->b_sh3[1] = props.f_rest[42].Read(plyData); fullPtr->b_sh3[2] = props.f_rest[43].Read(plyData); fullPtr->b_sh3[3] = props.f_rest[44].Read(plyData); } else { basePtr->r_sh0[0] = props.f_dc[0].Read(plyData); basePtr->r_sh0[1] = 0.0f; basePtr->r_sh0[2] = 0.0f; basePtr->r_sh0[3] = 0.0f; basePtr->g_sh0[0] = props.f_dc[1].Read(plyData); basePtr->g_sh0[1] = 0.0f; basePtr->g_sh0[2] = 0.0f; basePtr->g_sh0[3] = 0.0f; basePtr->b_sh0[0] = props.f_dc[2].Read(plyData); basePtr->b_sh0[1] = 0.0f; basePtr->b_sh0[2] = 0.0f; basePtr->b_sh0[3] = 0.0f; } // NOTE: scale is stored in logarithmic scale in plyFile float scale[3] = { expf(props.scale[0].Read(plyData)), expf(props.scale[1].Read(plyData)), expf(props.scale[2].Read(plyData)) }; float rot[4] = { props.rot[0].Read(plyData), props.rot[1].Read(plyData), props.rot[2].Read(plyData), props.rot[3].Read(plyData) }; glm::mat3 V = ComputeCovMatFromRotScale(rot, scale); basePtr->cov3_col0[0] = V[0][0]; basePtr->cov3_col0[1] = V[0][1]; basePtr->cov3_col0[2] = V[0][2]; basePtr->cov3_col1[0] = V[1][0]; basePtr->cov3_col1[1] = V[1][1]; basePtr->cov3_col1[2] = V[1][2]; basePtr->cov3_col2[0] = V[2][0]; basePtr->cov3_col2[1] = V[2][1]; basePtr->cov3_col2[2] = V[2][2]; i++; rawPtr += gaussianSize; }); } return true; } bool GaussianCloud::ExportPly(const std::string& plyFilename) const { std::ofstream plyFile(plyFilename, std::ios::binary); if (!plyFile.is_open()) { Log::E("failed to open %s\n", plyFilename.c_str()); return false; } Ply ply; ply.AddProperty("x", BinaryAttribute::Type::Float); ply.AddProperty("y", BinaryAttribute::Type::Float); ply.AddProperty("z", BinaryAttribute::Type::Float); ply.AddProperty("nx", BinaryAttribute::Type::Float); ply.AddProperty("ny", BinaryAttribute::Type::Float); ply.AddProperty("nz", BinaryAttribute::Type::Float); ply.AddProperty("f_dc_0", BinaryAttribute::Type::Float); ply.AddProperty("f_dc_1", BinaryAttribute::Type::Float); ply.AddProperty("f_dc_2", BinaryAttribute::Type::Float); if (opt.exportFullSH) { for (int i = 0; i < 45; i++) { ply.AddProperty("f_rest_" + std::to_string(i), BinaryAttribute::Type::Float); } } ply.AddProperty("opacity", BinaryAttribute::Type::Float); ply.AddProperty("scale_0", BinaryAttribute::Type::Float); ply.AddProperty("scale_1", BinaryAttribute::Type::Float); ply.AddProperty("scale_2", BinaryAttribute::Type::Float); ply.AddProperty("rot_0", BinaryAttribute::Type::Float); ply.AddProperty("rot_1", BinaryAttribute::Type::Float); ply.AddProperty("rot_2", BinaryAttribute::Type::Float); ply.AddProperty("rot_3", BinaryAttribute::Type::Float); struct { BinaryAttribute x, y, z; BinaryAttribute nx, ny, nz; BinaryAttribute f_dc[3]; BinaryAttribute f_rest[45]; BinaryAttribute opacity; BinaryAttribute scale[3]; BinaryAttribute rot[4]; } props; ply.GetProperty("x", props.x); ply.GetProperty("y", props.y); ply.GetProperty("z", props.z); ply.GetProperty("nx", props.nx); ply.GetProperty("ny", props.ny); ply.GetProperty("nz", props.nz); ply.GetProperty("f_dc_0", props.f_dc[0]); ply.GetProperty("f_dc_1", props.f_dc[1]); ply.GetProperty("f_dc_2", props.f_dc[2]); if (opt.exportFullSH) { for (int i = 0; i < 45; i++) { ply.GetProperty("f_rest_" + std::to_string(i), props.f_rest[i]); } } ply.GetProperty("opacity", props.opacity); ply.GetProperty("scale_0", props.scale[0]); ply.GetProperty("scale_1", props.scale[1]); ply.GetProperty("scale_2", props.scale[2]); ply.GetProperty("rot_0", props.rot[0]); ply.GetProperty("rot_1", props.rot[1]); ply.GetProperty("rot_2", props.rot[2]); ply.GetProperty("rot_3", props.rot[3]); ply.AllocData(numGaussians); uint8_t* gData = (uint8_t*)data.get(); size_t runningSize = 0; ply.ForEachVertexMut([this, &props, &gData, &runningSize](void* plyData, size_t size) { const float* posWithAlpha = posWithAlphaAttrib.Get(gData); const float* r_sh0 = r_sh0Attrib.Get(gData); const float* g_sh0 = g_sh0Attrib.Get(gData); const float* b_sh0 = b_sh0Attrib.Get(gData); const float* cov3_col0 = cov3_col0Attrib.Get(gData); props.x.Write(plyData, posWithAlpha[0]); props.y.Write(plyData, posWithAlpha[1]); props.z.Write(plyData, posWithAlpha[2]); props.nx.Write(plyData, 0.0f); props.ny.Write(plyData, 0.0f); props.nz.Write(plyData, 0.0f); props.f_dc[0].Write(plyData, r_sh0[0]); props.f_dc[1].Write(plyData, g_sh0[0]); props.f_dc[2].Write(plyData, b_sh0[0]); if (opt.exportFullSH) { // TODO: maybe just a raw memcopy would be faster for (int i = 0; i < 15; i++) { props.f_rest[i].Write(plyData, r_sh0[i + 1]); } for (int i = 0; i < 15; i++) { props.f_rest[i + 15].Write(plyData, g_sh0[i + 1]); } for (int i = 0; i < 15; i++) { props.f_rest[i + 30].Write(plyData, b_sh0[i + 1]); } } props.opacity.Write(plyData, ComputeOpacityFromAlpha(posWithAlpha[3])); glm::mat3 V(cov3_col0[0], cov3_col0[1], cov3_col0[2], cov3_col0[3], cov3_col0[4], cov3_col0[5], cov3_col0[6], cov3_col0[7], cov3_col0[8]); glm::quat rot; glm::vec3 scale; ComputeRotScaleFromCovMat(V, rot, scale); props.scale[0].Write(plyData, logf(scale.x)); props.scale[1].Write(plyData, logf(scale.y)); props.scale[2].Write(plyData, logf(scale.z)); props.rot[0].Write(plyData, rot.w); props.rot[1].Write(plyData, rot.x); props.rot[2].Write(plyData, rot.y); props.rot[3].Write(plyData, rot.z); gData += gaussianSize; runningSize += gaussianSize; assert(runningSize <= GetTotalSize()); // bad, we went off the end of the data ptr }); ply.Dump(plyFile); return true; } void GaussianCloud::InitDebugCloud() { const int NUM_SPLATS = 5; numGaussians = NUM_SPLATS * 3 + 1; gaussianSize = sizeof(FullGaussianData); InitAttribs(); FullGaussianData* gd = new FullGaussianData[numGaussians]; data.reset(gd); // // make an debug GaussianClound, that contain red, green and blue axes. // const float AXIS_LENGTH = 1.0f; const float DELTA = (AXIS_LENGTH / (float)NUM_SPLATS); const float COV_DIAG = 0.005f; const float SH_C0 = 0.28209479177387814f; const float SH_ONE = 1.0f / (2.0f * SH_C0); const float SH_ZERO = -1.0f / (2.0f * SH_C0); // x axis for (int i = 0; i < NUM_SPLATS; i++) { FullGaussianData g; memset(&g, 0, sizeof(FullGaussianData)); g.posWithAlpha[0] = i * DELTA + DELTA; g.posWithAlpha[1] = 0.0f; g.posWithAlpha[2] = 0.0f; g.posWithAlpha[3] = 1.0f; // red g.r_sh0[0] = SH_ONE; g.g_sh0[0] = SH_ZERO; g.b_sh0[0] = SH_ZERO; g.cov3_col0[0] = COV_DIAG; g.cov3_col1[1] = COV_DIAG; g.cov3_col2[2] = COV_DIAG; gd[i] = g; } // y axis for (int i = 0; i < NUM_SPLATS; i++) { FullGaussianData g; memset(&g, 0, sizeof(FullGaussianData)); g.posWithAlpha[0] = 0.0f; g.posWithAlpha[1] = i * DELTA + DELTA; g.posWithAlpha[2] = 0.0f; g.posWithAlpha[3] = 1.0f; // green g.r_sh0[0] = SH_ZERO; g.g_sh0[0] = SH_ONE; g.b_sh0[0] = SH_ZERO; g.cov3_col0[0] = COV_DIAG; g.cov3_col1[1] = COV_DIAG; g.cov3_col2[2] = COV_DIAG; gd[NUM_SPLATS + i] = g; } // z axis for (int i = 0; i < NUM_SPLATS; i++) { FullGaussianData g; memset(&g, 0, sizeof(FullGaussianData)); g.posWithAlpha[0] = 0.0f; g.posWithAlpha[1] = 0.0f; g.posWithAlpha[2] = i * DELTA + DELTA + 0.0001f; // AJT: HACK prevent div by zero for debug-shaders g.posWithAlpha[3] = 1.0f; // blue g.r_sh0[0] = SH_ZERO; g.g_sh0[0] = SH_ZERO; g.b_sh0[0] = SH_ONE; g.cov3_col0[0] = COV_DIAG; g.cov3_col1[1] = COV_DIAG; g.cov3_col2[2] = COV_DIAG; gd[(NUM_SPLATS * 2) + i] = g; } FullGaussianData g; memset(&g, 0, sizeof(FullGaussianData)); g.posWithAlpha[0] = 0.0f; g.posWithAlpha[1] = 0.0f; g.posWithAlpha[2] = 0.0f; g.posWithAlpha[3] = 1.0f; // white g.r_sh0[0] = SH_ONE; g.g_sh0[0] = SH_ONE; g.b_sh0[0] = SH_ONE; g.cov3_col0[0] = COV_DIAG; g.cov3_col1[1] = COV_DIAG; g.cov3_col2[2] = COV_DIAG; gd[(NUM_SPLATS * 3)] = g; } // only keep the nearest splats void GaussianCloud::PruneSplats(const glm::vec3& origin, uint32_t numSplats) { if (!data || static_cast(numSplats) >= numGaussians) { return; } using IndexDistPair = std::pair; std::vector indexDistVec; indexDistVec.reserve(numGaussians); uint8_t* rawPtr = (uint8_t*)data.get(); for (uint32_t i = 0; i < numGaussians; i++) { BaseGaussianData* basePtr = reinterpret_cast(rawPtr); glm::vec3 pos(basePtr->posWithAlpha[0], basePtr->posWithAlpha[1], basePtr->posWithAlpha[2]); indexDistVec.push_back(IndexDistPair(i, glm::distance(origin, pos))); rawPtr += gaussianSize; } std::sort(indexDistVec.begin(), indexDistVec.end(), [](const IndexDistPair& a, const IndexDistPair& b) { return a.second < b.second; }); uint8_t* newData; if (hasFullSH) { FullGaussianData* fullPtr = new FullGaussianData[numSplats]; newData = (uint8_t*)fullPtr; } else { BaseGaussianData* basePtr = new BaseGaussianData[numSplats]; newData = (uint8_t*)basePtr; } rawPtr = (uint8_t*)data.get(); uint8_t* rawPtr2 = newData; for (uint32_t i = 0; i < numSplats; i++) { memcpy(rawPtr2, rawPtr + indexDistVec[i].first * gaussianSize, gaussianSize); rawPtr2 += gaussianSize; } numGaussians = numSplats; data.reset(newData); } void GaussianCloud::ForEachPosWithAlpha(const ForEachPosWithAlphaCallback& cb) const { posWithAlphaAttrib.ForEach(GetRawDataPtr(), GetStride(), GetNumGaussians(), cb); } void GaussianCloud::InitAttribs() { // BaseGaussianData attribs posWithAlphaAttrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, posWithAlpha)}; r_sh0Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, r_sh0)}; g_sh0Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, g_sh0)}; b_sh0Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, b_sh0)}; cov3_col0Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, cov3_col0)}; cov3_col1Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, cov3_col1)}; cov3_col2Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, cov3_col2)}; // FullGaussianData attribs if (hasFullSH) { r_sh1Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, r_sh1)}; r_sh2Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, r_sh2)}; r_sh3Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, r_sh3)}; g_sh1Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, g_sh1)}; g_sh2Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, g_sh2)}; g_sh3Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, g_sh3)}; b_sh1Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, b_sh1)}; b_sh2Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, b_sh2)}; b_sh3Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, b_sh3)}; } } ================================================ FILE: src/gaussiancloud.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include #include "core/binaryattribute.h" class GaussianCloud { public: struct Options { bool importFullSH; bool exportFullSH; }; GaussianCloud(const Options& options); bool ImportPly(const std::string& plyFilename); bool ExportPly(const std::string& plyFilename) const; void InitDebugCloud(); // only keep the nearest splats void PruneSplats(const glm::vec3& origin, uint32_t numGaussians); size_t GetNumGaussians() const { return numGaussians; } size_t GetStride() const { return gaussianSize; } size_t GetTotalSize() const { return GetNumGaussians() * gaussianSize; } void* GetRawDataPtr() { return data.get(); } const void* GetRawDataPtr() const { return data.get(); } const BinaryAttribute& GetPosWithAlphaAttrib() const { return posWithAlphaAttrib; } const BinaryAttribute& GetR_SH0Attrib() const { return r_sh0Attrib; } const BinaryAttribute& GetR_SH1Attrib() const { return r_sh1Attrib; } const BinaryAttribute& GetR_SH2Attrib() const { return r_sh2Attrib; } const BinaryAttribute& GetR_SH3Attrib() const { return r_sh3Attrib; } const BinaryAttribute& GetG_SH0Attrib() const { return g_sh0Attrib; } const BinaryAttribute& GetG_SH1Attrib() const { return g_sh1Attrib; } const BinaryAttribute& GetG_SH2Attrib() const { return g_sh2Attrib; } const BinaryAttribute& GetG_SH3Attrib() const { return g_sh3Attrib; } const BinaryAttribute& GetB_SH0Attrib() const { return b_sh0Attrib; } const BinaryAttribute& GetB_SH1Attrib() const { return b_sh1Attrib; } const BinaryAttribute& GetB_SH2Attrib() const { return b_sh2Attrib; } const BinaryAttribute& GetB_SH3Attrib() const { return b_sh3Attrib; } const BinaryAttribute& GetCov3_Col0Attrib() const { return cov3_col0Attrib; } const BinaryAttribute& GetCov3_Col1Attrib() const { return cov3_col1Attrib; } const BinaryAttribute& GetCov3_Col2Attrib() const { return cov3_col2Attrib; } using ForEachPosWithAlphaCallback = std::function; void ForEachPosWithAlpha(const ForEachPosWithAlphaCallback& cb) const; bool HasFullSH() const { return hasFullSH; } protected: void InitAttribs(); std::shared_ptr data; BinaryAttribute posWithAlphaAttrib; BinaryAttribute r_sh0Attrib; BinaryAttribute r_sh1Attrib; BinaryAttribute r_sh2Attrib; BinaryAttribute r_sh3Attrib; BinaryAttribute g_sh0Attrib; BinaryAttribute g_sh1Attrib; BinaryAttribute g_sh2Attrib; BinaryAttribute g_sh3Attrib; BinaryAttribute b_sh0Attrib; BinaryAttribute b_sh1Attrib; BinaryAttribute b_sh2Attrib; BinaryAttribute b_sh3Attrib; BinaryAttribute cov3_col0Attrib; BinaryAttribute cov3_col1Attrib; BinaryAttribute cov3_col2Attrib; size_t numGaussians; size_t gaussianSize; Options opt; bool hasFullSH; }; ================================================ FILE: src/magiccarpet.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "magiccarpet.h" #ifdef __ANDROID__ #include #include #include #include #else #include #endif #include #include #include "core/image.h" #include "core/log.h" #include "core/util.h" const float SNAP_TIME = 1.0f; const float SNAP_ANGLE = glm::radians(30.0f); const float DOUBLE_GRIP_TIME = 0.1f; const float CARPET_RADIUS = 3.0f; const float CARPET_TILE_COUNT = 3.0f; glm::mat4 MagicCarpet::Pose::GetMat() const { return MakeMat4(rot, pos); } void MagicCarpet::Pose::Dump(const std::string& name) const { PrintVec(pos, name + ".pos"); Log::D("%s.posValid = %s, %s.posTracked = %s\n", name.c_str(), posValid ? "true" : "false", name.c_str(), posTracked ? "true" : "false"); PrintQuat(rot, name + ".rot"); Log::D("%s.rotValid = %s, %s.rotTracked = %s\n", name.c_str(), rotValid ? "true" : "false", name.c_str(), rotTracked ? "true" : "false"); } MagicCarpet::MagicCarpet(const glm::mat4& carpetMatIn, float moveSpeedIn) : sm(State::Normal), carpetMat(carpetMatIn), moveSpeed(moveSpeedIn) { // Normal sm.AddState(State::Normal, "Normal", [this]() { snapTimer = 0.0f; }, // enter [this]() {}, // exit [this](float dt) { NormalProcess(dt); }); // process sm.AddTransition(State::Normal, State::LeftGrip, "leftGrip down", [this]() { return in.buttonState.leftGrip; }); sm.AddTransition(State::Normal, State::RightGrip, "rightGrip down", [this]() { return in.buttonState.rightGrip; }); // LeftGrip sm.AddState(State::LeftGrip, "LeftGrip", [this]() { GrabPoses(); }, [this]() {}, [this](float dt) { if (GripCount() == 1) { gripTimer = DOUBLE_GRIP_TIME; } gripTimer -= dt; glm::mat4 grabMat = MakeMat4(grabLeftPose.rot, grabLeftPose.pos); glm::vec3 pos = in.leftPose.pos; glm::quat rot = grabLeftPose.rot; glm::mat4 currMat = MakeMat4(rot, pos); // adjust the carpet mat carpetMat = grabCarpetMat * grabMat * glm::inverse(currMat); }); sm.AddTransition(State::LeftGrip, State::Normal, "leftGrip up", [this]() { return !in.buttonState.leftGrip; }); sm.AddTransition(State::LeftGrip, State::DoubleGrip, "double grip", [this]() { return GripCount() == 2 && gripTimer < 0.0f; }); // RightGrip sm.AddState(State::RightGrip, "RightGrip", [this]() { GrabPoses(); }, [this]() {}, [this](float dt) { if (GripCount() == 1) { gripTimer = DOUBLE_GRIP_TIME; } gripTimer -= dt; glm::mat4 grabMat = MakeMat4(grabRightPose.rot, grabRightPose.pos); glm::vec3 pos = in.rightPose.pos; glm::quat rot = grabRightPose.rot; glm::mat4 currMat = MakeMat4(rot, pos); // adjust the carpet mat carpetMat = grabCarpetMat * grabMat * glm::inverse(currMat); }); sm.AddTransition(State::RightGrip, State::Normal, "rightGrip up", [this]() { return !in.buttonState.rightGrip; }); sm.AddTransition(State::RightGrip, State::DoubleGrip, "double grip", [this]() { return GripCount() == 2 && gripTimer < 0.0f; }); // DoubleGrip sm.AddState(State::DoubleGrip, "DoubleGrip", [this]() { GrabPoses(); scaleMode = (TriggerCount() > 0); }, [this]() {}, [this](float dt) { glm::vec3 d0 = grabRightPose.pos - grabLeftPose.pos; glm::vec3 p0 = glm::mix(grabLeftPose.pos, grabRightPose.pos, 0.5f); glm::vec3 x0 = SafeNormalize(d0, glm::vec3(1.0f, 0.0f, 0.0f)); glm::vec3 y0 = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 z0 = glm::normalize(glm::cross(x0, y0)); y0 = glm::normalize(glm::cross(z0, x0)); glm::mat4 grabMat(glm::vec4(x0, 0.0f), glm::vec4(y0, 0.0f), glm::vec4(z0, 0.0f), glm::vec4(p0, 1.0f)); glm::vec3 d1 = in.rightPose.pos - in.leftPose.pos; glm::vec3 p1 = glm::mix(in.leftPose.pos, in.rightPose.pos, 0.5f); glm::vec3 x1 = SafeNormalize(d1, glm::vec3(1.0f, 0.0f, 0.0f)); glm::vec3 y1 = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 z1 = glm::normalize(glm::cross(x1, y1)); y1 = glm::normalize(glm::cross(z1, x1)); glm::mat4 currMat(glm::vec4(x1, 0.0f), glm::vec4(y1, 0.0f), glm::vec4(z1, 0.0f), glm::vec4(p1, 1.0f)); float s1 = glm::length(d1) / glm::length(d0); if (scaleMode) { currMat *= MakeMat4(s1, glm::quat(1.0f, 0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f)); } // adjust the carpet mat carpetMat = grabCarpetMat * grabMat * glm::inverse(currMat); }); sm.AddTransition(State::DoubleGrip, State::LeftGrip, "grip count 1", [this]() { return GripCount() == 1 && !in.buttonState.rightGrip; }); sm.AddTransition(State::DoubleGrip, State::RightGrip, "grip count 1", [this]() { return GripCount() == 1 && !in.buttonState.leftGrip; }); sm.AddTransition(State::DoubleGrip, State::Normal, "grip count 0", [this]() { return GripCount() == 0; }); } bool MagicCarpet::Init(bool isFramebufferSRGBEnabledIn) { isFramebufferSRGBEnabled = isFramebufferSRGBEnabledIn; Image carpetImg; if (!carpetImg.Load("texture/carpet.png")) { Log::E("Error loading carpet.png\n"); return false; } carpetImg.isSRGB = isFramebufferSRGBEnabledIn; Texture::Params texParams = {FilterType::LinearMipmapLinear, FilterType::Linear, WrapType::Repeat, WrapType::Repeat}; carpetTex = std::make_shared(carpetImg, texParams); carpetProg = std::make_shared(); if (!carpetProg->LoadVertFrag("shader/carpet_vert.glsl", "shader/carpet_frag.glsl")) { Log::E("Error loading carpet shaders!\n"); return false; } carpetVao = std::make_shared(); std::vector posVec = { glm::vec3(-CARPET_RADIUS, 0.0f, -CARPET_RADIUS), glm::vec3(CARPET_RADIUS, 0.0f, -CARPET_RADIUS), glm::vec3(CARPET_RADIUS, 0.0f, CARPET_RADIUS), glm::vec3(-CARPET_RADIUS, 0.0f, CARPET_RADIUS) }; auto posBuffer = std::make_shared(GL_ARRAY_BUFFER, posVec); std::vector uvVec = { glm::vec2(0.0f, 0.0f), glm::vec2(CARPET_TILE_COUNT, 0.0f), glm::vec2(CARPET_TILE_COUNT, CARPET_TILE_COUNT), glm::vec2(0.0f, CARPET_TILE_COUNT) }; auto uvBuffer = std::make_shared(GL_ARRAY_BUFFER, uvVec); // build element array std::vector indexVec = { 0, 2, 1, 0, 3, 2 }; auto indexBuffer = std::make_shared(GL_ELEMENT_ARRAY_BUFFER, indexVec); // setup vertex array object with buffers carpetVao->SetAttribBuffer(carpetProg->GetAttribLoc("position"), posBuffer); carpetVao->SetAttribBuffer(carpetProg->GetAttribLoc("uv"), uvBuffer); carpetVao->SetElementBuffer(indexBuffer); return true; } void MagicCarpet::Process(const Pose& headPose, const Pose& leftPose, const Pose& rightPose, const glm::vec2& leftStick, const glm::vec2& rightStick, const ButtonState& buttonState, float dt) { /* glm::mat4 leftMat = carpetMat * leftPose.GetMat(); glm::mat4 rightMat = carpetMat * rightPose.GetMat(); DebugDraw_Transform(leftMat); DebugDraw_Transform(rightMat); */ in.headPose = headPose; in.leftPose = leftPose; in.rightPose = rightPose; in.leftStick = leftStick; in.rightStick = rightStick; in.buttonState = buttonState; sm.Process(dt); } void MagicCarpet::SetCarpetMat(const glm::mat4& carpetMatIn) { carpetMat = carpetMatIn; } void MagicCarpet::Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar) { carpetProg->Bind(); glm::mat4 modelViewMat = glm::inverse(cameraMat) * carpetMat; carpetProg->SetUniform("modelViewProjMat", projMat * modelViewMat); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, carpetTex->texture); carpetProg->SetUniform("colorTex", 0); carpetVao->DrawElements(GL_TRIANGLES); } void MagicCarpet::NormalProcess(float dt) { glm::vec3 horizVel; if (in.headPose.rotValid) { // get the forward and right vectors of the HMD glm::vec3 headForward = in.headPose.rot * glm::vec3(0.0f, 0.0f, -1.0f); glm::vec3 headRight = in.headPose.rot * glm::vec3(1.0f, 0.0f, 0.0f); // project the HMD forward & right vectors onto the carpet, i.e. make sure they lie in the horizontal plane // AJT: TODO: Handle bad normalize glm::vec3 horizForward = glm::normalize(glm::vec3(headForward.x, 0.0f, headForward.z)); glm::vec3 horizRight = glm::normalize(glm::vec3(headRight.x, 0.0f, headRight.z)); // use leftStick to move horizontally horizVel = horizForward * in.leftStick.y * moveSpeed + horizRight * in.leftStick.x * moveSpeed; } else { horizVel = glm::vec3(0.0f, 0.0f, 0.0f); } // handle snap turns snapTimer -= dt; if (fabs(in.rightStick.x) > 0.5f && snapTimer < 0.0f && in.headPose.posValid && in.headPose.posTracked) { // snap! float snapSign = in.rightStick.x > 0.0f ? -1.0f : 1.0f; // Rotate the carpet around the users HMD glm::vec3 snapPos = XformPoint(carpetMat, in.headPose.pos); glm::quat snapRot = glm::angleAxis(snapSign * SNAP_ANGLE, glm::normalize(XformVec(carpetMat, glm::vec3(0.0f, 1.0f, 0.0f)))); carpetMat = MakeRotateAboutPointMat(snapPos, snapRot) * carpetMat; snapTimer = SNAP_TIME; } else if (fabs(in.rightStick.x) < 0.2f) { // reset snap snapTimer = 0.0f; } // move the carpet! glm::vec3 vel = XformVec(carpetMat, horizVel); carpetMat[3][0] += vel.x * dt; carpetMat[3][1] += vel.y * dt; carpetMat[3][2] += vel.z * dt; } void MagicCarpet::GrabPoses() { grabLeftPose = in.leftPose; grabRightPose = in.rightPose; grabCarpetMat = carpetMat; } int MagicCarpet::GripCount() const { int count = 0; count += (in.buttonState.leftGrip) ? 1 : 0; count += (in.buttonState.rightGrip) ? 1 : 0; return count; } int MagicCarpet::TriggerCount() const { int count = 0; count += (in.buttonState.leftTrigger) ? 1 : 0; count += (in.buttonState.rightTrigger) ? 1 : 0; return count; } ================================================ FILE: src/magiccarpet.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include "core/program.h" #include "core/statemachine.h" #include "core/texture.h" #include "core/vertexbuffer.h" // VR flycam class MagicCarpet { public: MagicCarpet(const glm::mat4& carpetMatIn, float moveSpeedIn); bool Init(bool isFramebufferSRGBEnabledIn); struct Pose { Pose() : pos(), rot(), posValid(false), posTracked(false), rotValid(false), rotTracked(false) {} glm::mat4 GetMat() const; void Dump(const std::string& name) const; glm::vec3 pos; glm::quat rot; bool posValid; bool posTracked; bool rotValid; bool rotTracked; }; struct ButtonState { ButtonState() : leftTrigger(false), rightTrigger(false), leftGrip(false), rightGrip(false) {} bool leftTrigger; bool rightTrigger; bool leftGrip; bool rightGrip; }; void Process(const Pose& headPose, const Pose& leftPose, const Pose& rightPose, const glm::vec2& leftStick, const glm::vec2& rightStick, const ButtonState& buttonState, float dt); const glm::mat4& GetCarpetMat() const { return carpetMat; } void SetCarpetMat(const glm::mat4& carpetMatIn); void Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar); protected: void NormalProcess(float dt); void GrabPoses(); int GripCount() const; int TriggerCount() const; enum class State { Normal, LeftGrip, RightGrip, DoubleGrip }; struct InputContext { Pose headPose; Pose leftPose; Pose rightPose; glm::vec2 leftStick; glm::vec2 rightStick; ButtonState buttonState; }; float moveSpeed; StateMachine sm; InputContext in; // used in normal state to perform snap turns float snapTimer; // used in grip states float gripTimer; // used in double grip state bool scaleMode; // used in grab states to store the pos/rot of controllers on entry into the state. Pose grabLeftPose; Pose grabRightPose; glm::mat4 grabCarpetMat; glm::mat4 carpetMat; std::shared_ptr carpetTex; std::shared_ptr carpetProg; std::shared_ptr carpetVao; bool isFramebufferSRGBEnabled; }; ================================================ FILE: src/maincontext.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #if defined(__ANDROID__) #include #include #include #include #include #include #elif defined(__linux__) #include #include #include // for fullscreen video mode #include // for resolution changes #include #include // Conficts with core/optionparser.h #ifdef None #undef None #endif #endif #if defined(__ANDROID__) struct MainContext { EGLDisplay display; EGLConfig config; EGLContext context; android_app* androidApp; }; #elif defined(__linux__) struct MainContext { Display* xdisplay; uint32_t visualid; GLXFBConfig glxFBConfig; GLXDrawable glxDrawable; GLXContext glxContext; }; #else struct MainContext { }; #endif ================================================ FILE: src/ply.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "ply.h" #include #include #include #include #ifdef TRACY_ENABLE #include #else #define ZoneScoped #define ZoneScopedNC(NAME, COLOR) #endif #include "core/log.h" static bool CheckLine(std::ifstream& plyFile, const std::string& validLine) { std::string line; return std::getline(plyFile, line) && line == validLine; } static bool GetNextPlyLine(std::ifstream& plyFile, std::string& lineOut) { while (std::getline(plyFile, lineOut)) { // skip comment lines if (lineOut.find("comment", 0) != 0) { return true; } } return false; } static const char* BinaryAttributeTypeToString(BinaryAttribute::Type type) { switch (type) { case BinaryAttribute::Type::Char: return "char"; case BinaryAttribute::Type::UChar: return "uchar"; case BinaryAttribute::Type::Short: return "short"; case BinaryAttribute::Type::UShort: return "ushort"; case BinaryAttribute::Type::Int: return "int"; case BinaryAttribute::Type::UInt: return "uint"; case BinaryAttribute::Type::Float: return "float"; case BinaryAttribute::Type::Double: return "double"; default: assert(false); // bad attribute type return "unknown"; }; } Ply::Ply() : vertexCount(0), vertexSize(0) { ; } bool Ply::Parse(std::ifstream& plyFile) { if (!ParseHeader(plyFile)) { return false; } // read rest of file into data ptr { ZoneScopedNC("Ply::Parse() read data", tracy::Color::Yellow); AllocData(vertexCount); plyFile.read((char*)data.get(), vertexSize * vertexCount); } return true; } void Ply::Dump(std::ofstream& plyFile) const { DumpHeader(plyFile); plyFile.write((char*)data.get(), vertexSize * vertexCount); } bool Ply::GetProperty(const std::string& key, BinaryAttribute& binaryAttributeOut) const { auto iter = propertyMap.find(key); if (iter != propertyMap.end()) { binaryAttributeOut = iter->second; return true; } return false; } void Ply::AddProperty(const std::string& key, BinaryAttribute::Type type) { using PropInfoPair = std::pair; BinaryAttribute attrib(type, vertexSize); propertyMap.emplace(PropInfoPair(key, attrib)); vertexSize += attrib.size; } void Ply::AllocData(size_t numVertices) { vertexCount = numVertices; data.reset(new uint8_t[vertexSize * numVertices]); } void Ply::ForEachVertex(const VertexCallback& cb) const { const uint8_t* ptr = data.get(); for (size_t i = 0; i < vertexCount; i++) { cb(ptr, vertexSize); ptr += vertexSize; } } void Ply::ForEachVertexMut(const VertexCallbackMut& cb) { uint8_t* ptr = data.get(); for (size_t i = 0; i < vertexCount; i++) { cb(ptr, vertexSize); ptr += vertexSize; } } bool Ply::ParseHeader(std::ifstream& plyFile) { ZoneScopedNC("Ply::ParseHeader", tracy::Color::Green); // validate start of header std::string token1, token2, token3; // check header starts with "ply". if (!GetNextPlyLine(plyFile, token1)) { Log::E("Unexpected error reading next line\n"); return false; } if (token1 != "ply") { Log::E("Invalid ply file\n"); return false; } // check format if (!GetNextPlyLine(plyFile, token1)) { Log::E("Unexpected error reading next line\n"); return false; } if (token1 != "format binary_little_endian 1.0" && token1 != "format binary_big_endian 1.0") { Log::E("Invalid ply file, expected format\n"); return false; } if (token1 != "format binary_little_endian 1.0") { Log::E("Unsupported ply file, only binary_little_endian supported\n"); return false; } // parse "element vertex {number}" std::string line; if (!GetNextPlyLine(plyFile, line)) { Log::E("Unexpected error reading next line\n"); return false; } std::istringstream iss(line); if (!((iss >> token1 >> token2 >> vertexCount) && (token1 == "element") && (token2 == "vertex"))) { Log::E("Invalid ply file, expected \"element vertex {number}\"\n"); return false; } // TODO: support other "element" types faces, edges etc? // at the moment I only care about ply files with vertex elements. while (true) { if (!GetNextPlyLine(plyFile, line)) { Log::E("unexpected error reading line\n"); return false; } if (line == "end_header") { break; } iss.str(line); iss.clear(); iss >> token1 >> token2 >> token3; if (token1 != "property") { Log::E("Invalid header, expected property\n"); return false; } if (token2 == "char" || token2 == "int8") { AddProperty(token3, BinaryAttribute::Type::Char); } else if (token2 == "uchar" || token2 == "uint8") { AddProperty(token3, BinaryAttribute::Type::UChar); } else if (token2 == "short" || token2 == "int16") { AddProperty(token3, BinaryAttribute::Type::Short); } else if (token2 == "ushort" || token2 == "uint16") { AddProperty(token3, BinaryAttribute::Type::UShort); } else if (token2 == "int" || token2 == "int32") { AddProperty(token3, BinaryAttribute::Type::Int); } else if (token2 == "uint" || token2 == "uint32") { AddProperty(token3, BinaryAttribute::Type::UInt); } else if (token2 == "float" || token2 == "float32") { AddProperty(token3, BinaryAttribute::Type::Float); } else if (token2 == "double" || token2 == "float64") { AddProperty(token3, BinaryAttribute::Type::Double); } else { Log::E("Unsupported type \"%s\" for property \"%s\"\n", token2.c_str(), token3.c_str()); return false; } } return true; } void Ply::DumpHeader(std::ofstream& plyFile) const { // ply files have unix line endings. plyFile << "ply\n"; plyFile << "format binary_little_endian 1.0\n"; plyFile << "element vertex " << vertexCount << "\n"; // sort properties by offset using PropInfoPair = std::pair; std::vector propVec; propVec.reserve(propertyMap.size()); for (auto& pair : propertyMap) { propVec.push_back(pair); } std::sort(propVec.begin(), propVec.end(), [](const PropInfoPair& a, const PropInfoPair& b) { return a.second.offset < b.second.offset; }); for (auto& pair : propVec) { plyFile << "property " << BinaryAttributeTypeToString(pair.second.type) << " " << pair.first << "\n"; } plyFile << "end_header\n"; } ================================================ FILE: src/ply.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include #include #include #include "core/binaryattribute.h" class Ply { public: Ply(); bool Parse(std::ifstream& plyFile); void Dump(std::ofstream& plyFile) const; bool GetProperty(const std::string& key, BinaryAttribute& attributeOut) const; void AddProperty(const std::string& key, BinaryAttribute::Type type); void AllocData(size_t numVertices); using VertexCallback = std::function; void ForEachVertex(const VertexCallback& cb) const; using VertexCallbackMut = std::function; void ForEachVertexMut(const VertexCallbackMut& cb); size_t GetVertexCount() const { return vertexCount; } protected: bool ParseHeader(std::ifstream& plyFile); void DumpHeader(std::ofstream& plyFile) const; std::unordered_map propertyMap; std::unique_ptr data; size_t vertexCount; size_t vertexSize; }; ================================================ FILE: src/pointcloud.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "pointcloud.h" #include #include #include #include #include #include "core/log.h" #include "core/util.h" #include "ply.h" struct PointData { PointData() noexcept {} float position[4]; float color[4]; }; PointCloud::PointCloud(bool useLinearColorsIn) : numPoints(0), pointSize(0), useLinearColors(useLinearColorsIn) { ; } bool PointCloud::ImportPly(const std::string& plyFilename) { std::ifstream plyFile(plyFilename, std::ios::binary); if (!plyFile.is_open()) { Log::E("failed to open \"%s\"\n", plyFilename.c_str()); return false; } Ply ply; if (!ply.Parse(plyFile)) { Log::E("Error parsing ply file \"%s\"\n", plyFilename.c_str()); return false; } struct { BinaryAttribute x, y, z; BinaryAttribute red, green, blue; } props; if (!ply.GetProperty("x", props.x) || !ply.GetProperty("y", props.y) || !ply.GetProperty("z", props.z)) { Log::E("Error parsing ply file \"%s\", missing position property\n", plyFilename.c_str()); } bool useDoubles = (props.x.type == BinaryAttribute::Type::Double && props.y.type == BinaryAttribute::Type::Double && props.z.type == BinaryAttribute::Type::Double); if (!ply.GetProperty("red", props.red) || !ply.GetProperty("green", props.green) || !ply.GetProperty("blue", props.blue)) { Log::E("Error parsing ply file \"%s\", missing color property\n", plyFilename.c_str()); } numPoints = ply.GetVertexCount(); pointSize = sizeof(PointData); InitAttribs(); PointData* pd = new PointData[numPoints]; data.reset(pd); if (useDoubles) { int i = 0; ply.ForEachVertex([this, pd, &i, &props](const void* data, size_t size) { if (useLinearColors) { pd[i].position[0] = SRGBToLinear((float)props.x.Read(data)); pd[i].position[1] = SRGBToLinear((float)props.y.Read(data)); pd[i].position[2] = SRGBToLinear((float)props.z.Read(data)); } else { pd[i].position[0] = (float)props.x.Read(data); pd[i].position[1] = (float)props.y.Read(data); pd[i].position[2] = (float)props.z.Read(data); } pd[i].position[3] = 1.0f; pd[i].color[0] = (float)props.red.Read(data) / 255.0f; pd[i].color[1] = (float)props.green.Read(data) / 255.0f; pd[i].color[2] = (float)props.blue.Read(data) / 255.0f; pd[i].color[3] = 1.0f; i++; }); } else { int i = 0; ply.ForEachVertex([this, pd, &i, &props](const void* data, size_t size) { if (useLinearColors) { pd[i].position[0] = SRGBToLinear(props.x.Read(data)); pd[i].position[1] = SRGBToLinear(props.y.Read(data)); pd[i].position[2] = SRGBToLinear(props.z.Read(data)); } else { pd[i].position[0] = props.x.Read(data); pd[i].position[1] = props.y.Read(data); pd[i].position[2] = props.z.Read(data); } pd[i].position[3] = 1.0f; pd[i].color[0] = (float)props.red.Read(data) / 255.0f; pd[i].color[1] = (float)props.green.Read(data) / 255.0f; pd[i].color[2] = (float)props.blue.Read(data) / 255.0f; pd[i].color[3] = 1.0f; i++; }); } return true; } bool PointCloud::ExportPly(const std::string& plyFilename) const { std::ofstream plyFile(plyFilename, std::ios::binary); if (!plyFile.is_open()) { Log::E("failed to open %s\n", plyFilename.c_str()); return false; } Ply ply; ply.AddProperty("x", BinaryAttribute::Type::Float); ply.AddProperty("y", BinaryAttribute::Type::Float); ply.AddProperty("z", BinaryAttribute::Type::Float); ply.AddProperty("nx", BinaryAttribute::Type::Float); ply.AddProperty("ny", BinaryAttribute::Type::Float); ply.AddProperty("nz", BinaryAttribute::Type::Float); ply.AddProperty("red", BinaryAttribute::Type::UChar); ply.AddProperty("green", BinaryAttribute::Type::UChar); ply.AddProperty("blue", BinaryAttribute::Type::UChar); struct { BinaryAttribute x, y, z; BinaryAttribute nx, ny, nz; BinaryAttribute red, green, blue; } props; ply.GetProperty("x", props.x); ply.GetProperty("y", props.y); ply.GetProperty("z", props.z); ply.GetProperty("nx", props.nx); ply.GetProperty("ny", props.ny); ply.GetProperty("nz", props.nz); ply.GetProperty("red", props.red); ply.GetProperty("green", props.green); ply.GetProperty("blue", props.blue); ply.AllocData(numPoints); uint8_t* cloudData = (uint8_t*)data.get(); size_t runningSize = 0; ply.ForEachVertexMut([this, &props, &cloudData, &runningSize](void* plyData, size_t size) { const float* position = positionAttrib.Get(cloudData); const float* color = colorAttrib.Get(cloudData); props.x.Write(plyData, position[0]); props.y.Write(plyData, position[1]); props.z.Write(plyData, position[2]); props.nx.Write(plyData, 0.0f); props.ny.Write(plyData, 0.0f); props.nz.Write(plyData, 0.0f); props.red.Write(plyData, (uint8_t)(color[0] * 255.0f)); props.green.Write(plyData, (uint8_t)(color[1] * 255.0f)); props.blue.Write(plyData, (uint8_t)(color[2] * 255.0f)); cloudData += pointSize; runningSize += pointSize; assert(runningSize <= GetTotalSize()); // bad, we went outside of data ptr contents. }); ply.Dump(plyFile); return true; } void PointCloud::InitDebugCloud() { const int NUM_POINTS = 5; numPoints = NUM_POINTS * 3; pointSize = sizeof(PointData); InitAttribs(); PointData* pd = new PointData[numPoints]; data.reset(pd); // // make an debug pointVec, that contains three lines one for each axis. // const float AXIS_LENGTH = 1.0f; const float DELTA = (AXIS_LENGTH / (float)NUM_POINTS); // x axis for (int i = 0; i < NUM_POINTS; i++) { PointData& p = pd[i]; p.position[0] = i * DELTA; p.position[1] = 0.0f; p.position[2] = 0.0f; p.position[3] = 1.0f; p.color[0] = 1.0f; p.color[1] = 0.0f; p.color[2] = 0.0f; p.color[3] = 1.0f; } // y axis for (int i = 0; i < NUM_POINTS; i++) { PointData& p = pd[i + NUM_POINTS]; p.position[0] = 0.0f; p.position[1] = i * DELTA; p.position[2] = 0.0f; p.position[3] = 1.0f; p.color[0] = 0.0f; p.color[1] = 1.0f; p.color[2] = 0.0f; p.color[3] = 1.0f; } // z axis for (int i = 0; i < NUM_POINTS; i++) { PointData& p = pd[(2 * NUM_POINTS) + i]; p.position[0] = 0.0f; p.position[1] = 0.0f; p.position[2] = i * DELTA; p.position[3] = 1.0f; p.color[0] = 0.0f; p.color[1] = 0.0f; p.color[2] = 1.0f; p.color[3] = 1.0f; } } void PointCloud::ForEachPosition(const ForEachPositionCallback& cb) const { positionAttrib.ForEach(GetRawDataPtr(), GetStride(), GetNumPoints(), cb); } void PointCloud::InitAttribs() { positionAttrib = {BinaryAttribute::Type::Float, offsetof(PointData, position)}; colorAttrib = {BinaryAttribute::Type::Float, offsetof(PointData, color)}; } ================================================ FILE: src/pointcloud.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include "core/binaryattribute.h" class PointCloud { public: PointCloud(bool useLinearColorsIn); bool ImportPly(const std::string& plyFilename); bool ExportPly(const std::string& plyFilename) const; void InitDebugCloud(); size_t GetNumPoints() const { return numPoints; } size_t GetStride() const { return pointSize; } size_t GetTotalSize() const { return GetNumPoints() * GetStride(); } void* GetRawDataPtr() { return data.get(); } const void* GetRawDataPtr() const { return data.get(); } const BinaryAttribute& GetPositionAttrib() const { return positionAttrib; } const BinaryAttribute& GetColorAttrib() const { return colorAttrib; } using ForEachPositionCallback = std::function; void ForEachPosition(const ForEachPositionCallback& cb) const; protected: void InitAttribs(); std::shared_ptr data; BinaryAttribute positionAttrib; BinaryAttribute colorAttrib; size_t numPoints; size_t pointSize; bool useLinearColors; }; ================================================ FILE: src/pointrenderer.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "pointrenderer.h" #ifdef __ANDROID__ #include #include #include #include #else #include #endif #include #ifdef TRACY_ENABLE #include #else #define ZoneScoped #define ZoneScopedNC(NAME, COLOR) #endif #include "core/image.h" #include "core/log.h" #include "core/texture.h" #include "core/util.h" #include "radix_sort.hpp" static void SetupAttrib(int loc, const BinaryAttribute& attrib, int32_t numElems, size_t stride) { assert(attrib.type == BinaryAttribute::Type::Float); glVertexAttribPointer(loc, numElems, GL_FLOAT, GL_FALSE, (uint32_t)stride, (void*)attrib.offset); glEnableVertexAttribArray(loc); } PointRenderer::PointRenderer() { } PointRenderer::~PointRenderer() { } bool PointRenderer::Init(std::shared_ptr pointCloud, bool isFramebufferSRGBEnabledIn) { GL_ERROR_CHECK("PointRenderer::Init() begin"); isFramebufferSRGBEnabled = isFramebufferSRGBEnabledIn; Image pointImg; if (!pointImg.Load("texture/sphere.png")) { Log::E("Error loading sphere.png\n"); return false; } pointImg.isSRGB = isFramebufferSRGBEnabled; Texture::Params texParams = {FilterType::LinearMipmapLinear, FilterType::Linear, WrapType::ClampToEdge, WrapType::ClampToEdge}; pointTex = std::make_shared(pointImg, texParams); pointProg = std::make_shared(); if (!pointProg->LoadVertGeomFrag("shader/point_vert.glsl", "shader/point_geom.glsl", "shader/point_frag.glsl")) { Log::E("Error loading point shaders!\n"); return false; } preSortProg = std::make_shared(); if (!preSortProg->LoadCompute("shader/presort_compute.glsl")) { Log::E("Error loading point pre-sort compute shader!\n"); return false; } const size_t numPoints = pointCloud->GetNumPoints(); // build posVec posVec.reserve(numPoints); pointCloud->ForEachPosition([this](const float* pos) { posVec.emplace_back(glm::vec4(pos[0], pos[1], pos[2], pos[3])); }); BuildVertexArrayObject(pointCloud); depthVec.resize(numPoints); keyBuffer = std::make_shared(GL_SHADER_STORAGE_BUFFER, depthVec, GL_DYNAMIC_STORAGE_BIT); valBuffer = std::make_shared(GL_SHADER_STORAGE_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT); posBuffer = std::make_shared(GL_SHADER_STORAGE_BUFFER, posVec); sorter = std::make_shared(numPoints); atomicCounterVec.resize(1, 0); atomicCounterBuffer = std::make_shared(GL_ATOMIC_COUNTER_BUFFER, atomicCounterVec, GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT); GL_ERROR_CHECK("PointRenderer::Init() end"); return true; } void PointRenderer::Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar) { ZoneScoped; GL_ERROR_CHECK("PointRenderer::Render() begin"); const size_t numPoints = posVec.size(); glm::mat4 modelViewMat = glm::inverse(cameraMat); const uint32_t MAX_DEPTH = std::numeric_limits::max(); { ZoneScopedNC("pre-sort", tracy::Color::Red4); preSortProg->Bind(); preSortProg->SetUniform("modelViewProj", projMat * modelViewMat); preSortProg->SetUniform("nearFar", nearFar); preSortProg->SetUniform("keyMax", MAX_DEPTH); glm::mat4 modelViewProjMat = projMat * modelViewMat; // reset counter back to 0 atomicCounterVec[0] = 0; atomicCounterBuffer->Update(atomicCounterVec); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, posBuffer->GetObj()); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, keyBuffer->GetObj()); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, valBuffer->GetObj()); glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 4, atomicCounterBuffer->GetObj()); const int LOCAL_SIZE = 256; glDispatchCompute(((GLuint)numPoints + (LOCAL_SIZE - 1)) / LOCAL_SIZE, 1, 1); // Assuming LOCAL_SIZE threads per group glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT); GL_ERROR_CHECK("PointRenderer::Render() pre-sort"); } uint32_t sortCount = 0; { ZoneScopedNC("get-count", tracy::Color::Green); atomicCounterBuffer->Read(atomicCounterVec); sortCount = atomicCounterVec[0]; assert(sortCount <= (uint32_t)numPoints); GL_ERROR_CHECK("PointRenderer::Render() get-count"); } { ZoneScopedNC("sort", tracy::Color::Red4); sorter->sort(keyBuffer->GetObj(), valBuffer->GetObj(), sortCount); GL_ERROR_CHECK("PointRenderer::Render() sort"); } { ZoneScopedNC("copy-sorted", tracy::Color::DarkGreen); glBindBuffer(GL_COPY_READ_BUFFER, valBuffer->GetObj()); glBindBuffer(GL_COPY_WRITE_BUFFER, pointVao->GetElementBuffer()->GetObj()); glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sortCount * sizeof(uint32_t)); GL_ERROR_CHECK("PointRenderer::Render() copy-sorted"); } { ZoneScopedNC("draw", tracy::Color::Red4); float width = viewport.z; float height = viewport.w; float aspectRatio = width / height; pointProg->Bind(); pointProg->SetUniform("modelViewMat", modelViewMat); pointProg->SetUniform("projMat", projMat); pointProg->SetUniform("pointSize", 0.02f); // in ndc space?!? pointProg->SetUniform("invAspectRatio", 1.0f / aspectRatio); // use texture unit 0 for colorTexture glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, pointTex->texture); pointProg->SetUniform("colorTex", 0); pointVao->Bind(); glDrawElements(GL_POINTS, sortCount, GL_UNSIGNED_INT, nullptr); pointVao->Unbind(); GL_ERROR_CHECK("PointRenderer::Render() draw"); } } void PointRenderer::BuildVertexArrayObject(std::shared_ptr pointCloud) { pointVao = std::make_shared(); const size_t numPoints = pointCloud->GetNumPoints(); // allocate large buffer to hold interleaved vertex data pointDataBuffer = std::make_shared(GL_ARRAY_BUFFER, pointCloud->GetRawDataPtr(), pointCloud->GetTotalSize(), 0); // build element array indexVec.reserve(numPoints); assert(numPoints <= std::numeric_limits::max()); for (uint32_t i = 0; i < (uint32_t)numPoints; i++) { indexVec.push_back(i); } auto indexBuffer = std::make_shared(GL_ELEMENT_ARRAY_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT); pointVao->Bind(); pointDataBuffer->Bind(); SetupAttrib(pointProg->GetAttribLoc("position"), pointCloud->GetPositionAttrib(), 4, pointCloud->GetStride()); SetupAttrib(pointProg->GetAttribLoc("color"), pointCloud->GetColorAttrib(), 4, pointCloud->GetStride()); pointVao->SetElementBuffer(indexBuffer); pointDataBuffer->Unbind(); } ================================================ FILE: src/pointrenderer.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include "core/program.h" #include "core/texture.h" #include "core/vertexbuffer.h" #include "pointcloud.h" namespace rgc::radix_sort { struct sorter; } class PointRenderer { public: PointRenderer(); ~PointRenderer(); bool Init(std::shared_ptr pointCloud, bool isFramebufferSRGBEnabledIn); // viewport = (x, y, width, height) void Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar); protected: void BuildVertexArrayObject(std::shared_ptr pointCloud); std::shared_ptr pointTex; std::shared_ptr pointProg; std::shared_ptr preSortProg; std::shared_ptr pointVao; std::shared_ptr pointDataBuffer; std::vector indexVec; std::vector depthVec; std::vector posVec; std::vector atomicCounterVec; std::shared_ptr keyBuffer; std::shared_ptr valBuffer; std::shared_ptr posBuffer; std::shared_ptr atomicCounterBuffer; std::shared_ptr sorter; bool isFramebufferSRGBEnabled; }; ================================================ FILE: src/radix_sort.hpp ================================================ /* MIT License Copyright (c) 2021 Lorenzo Rutayisire Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. https://github.com/loryruta/gpu-radix-sort */ #pragma once #ifdef __ANDROID__ #include #include #include #include #include #else #include // Implement your own OpenGL functions loading library. #endif #include #include #include #include #ifdef __ANDROID__ // glBufferStorage is not present in OpenGLES 3.2 static void glBufferStorage(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) { GLenum usage = 0; if (flags & GL_DYNAMIC_STORAGE_BIT) { if (flags & GL_MAP_READ_BIT) { usage = GL_DYNAMIC_READ; } else { usage = GL_DYNAMIC_DRAW; } } else { if (flags & GL_MAP_READ_BIT) { usage = GL_STATIC_READ; } else { usage = GL_STATIC_DRAW; } } glBufferData(target, size, data, usage); } static void ZeroBuffer(GLenum target, size_t size) { static std::vector zeroVec; zeroVec.resize(size, 0); glBufferSubData(target, 0, size, (void*)zeroVec.data()); } #endif #define RGC_RADIX_SORT_THREADS_PER_BLOCK 64 #define RGC_RADIX_SORT_ITEMS_PER_THREAD 4 #define RGC_RADIX_SORT_BITSET_NUM 4 #define RGC_RADIX_SORT_BITSET_COUNT ((sizeof(GLuint) * 8) / RGC_RADIX_SORT_BITSET_NUM) #define RGC_RADIX_SORT_BITSET_SIZE GLuint(exp2(RGC_RADIX_SORT_BITSET_NUM)) #ifdef RGC_RADIX_SORT_DEBUG #include "renderdoc.hpp" #define RGC_RADIX_SORT_RENDERDOC_WATCH(capture, f) rgc::renderdoc::watch(capture, f) #else #define RGC_RADIX_SORT_RENDERDOC_WATCH(capture, f) f() #endif inline void __rgc_shader_injector_load_src(GLuint shader, char const* src) { #ifdef __ANDROID__ std::string version = "#version 320 es\n"; #else std::string version = "#version 460\n"; #endif std::string source = version + src; const GLchar* sourcePtr = source.c_str(); glShaderSource(shader, 1, (const GLchar**)&sourcePtr, nullptr); } inline void __rgc_shader_injector_load_src_from_file(GLuint shader, std::filesystem::path const& filename) { std::ifstream file(filename); if (!file.is_open()) { std::cerr << "Failed to open file at: " << filename << std::endl; throw std::invalid_argument("Failed to open text file."); } std::string src((std::istreambuf_iterator(file)), std::istreambuf_iterator()); __rgc_shader_injector_load_src(shader, src.c_str()); } #define RGC_SHADER_INJECTOR_INJECTION_POINT #define RGC_SHADER_INJECTOR_LOAD_SRC(shader, path) __rgc_shader_injector_load_src_from_file(shader, path) inline const char* __rgc_shader_injector_shader_src_6213812085af7087c07246dabc62da60 = "\n#define THREAD_IDX gl_LocalInvocationIndex\n#define THREADS_NUM 64\n#define THREAD_BLOCK_IDX (gl_WorkGroupID.x + gl_NumWorkGroups.x * (gl_WorkGroupID.y + gl_NumWorkGroups.z * gl_WorkGroupID.z))\n#define THREAD_BLOCKS_NUM (gl_NumWorkGroups.x * gl_NumWorkGroups.y * gl_NumWorkGroups.z)\n#define ITEMS_NUM 4u\n\n#define BITSET_NUM 4u\n#define BITSET_SIZE 16u\n\nlayout(local_size_x = THREADS_NUM, local_size_y = 1, local_size_z = 1) in;\n\nlayout(std430, binding = 0) buffer ssbo_key { uint b_key_buf[]; };\nlayout(std430, binding = 1) buffer ssbo_count_buf { uint b_count_buf[]; }; // [THREAD_BLOCKS_NUM * BITSET_SIZE]\nlayout(std430, binding = 2) buffer ssbo_tot_count_buf { uint b_tot_count_buf[BITSET_SIZE]; };\n\nuniform uint u_arr_len;\nuniform uint u_bitset_idx;\n\nuint to_partition_radixes_offsets_idx(uint radix, uint thread_block_idx)\n{\n uint pow_of_2_thread_blocks_num = uint(exp2(ceil(log2(float(THREAD_BLOCKS_NUM)))));\n return radix * pow_of_2_thread_blocks_num + thread_block_idx;\n}\n\nuint to_loc_idx(uint item_idx, uint thread_idx)\n{\n return (thread_idx * ITEMS_NUM + item_idx);\n}\n\nuint to_key_idx(uint item_idx, uint thread_idx, uint thread_block_idx)\n{\n return (thread_block_idx * ITEMS_NUM * uint(THREADS_NUM)) + (thread_idx * ITEMS_NUM) + item_idx;\n}\n\nvoid main()\n{\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\n if (key_idx >= u_arr_len) {\n continue;\n }\n\n uint bitset_mask = (BITSET_SIZE - 1u) << (BITSET_NUM * u_bitset_idx);\n uint rad = (b_key_buf[key_idx] & bitset_mask) >> (BITSET_NUM * u_bitset_idx);\n\n atomicAdd(b_count_buf[to_partition_radixes_offsets_idx(rad, THREAD_BLOCK_IDX)], 1u);\n atomicAdd(b_tot_count_buf[rad], 1u);\n }\n}\n"; inline const char* __rgc_shader_injector_shader_src_9992419d7253eb1e5310935c952c8eff = "\n#define THREAD_IDX gl_LocalInvocationIndex\n#define THREADS_NUM 64\n#define THREAD_BLOCK_IDX (gl_WorkGroupID.x + gl_NumWorkGroups.x * (gl_WorkGroupID.y + gl_NumWorkGroups.z * gl_WorkGroupID.z))\n#define ITEMS_NUM 4u\n#define BITSET_NUM 4u\n#define BITSET_SIZE 16u\n\n#define OP_UPSWEEP 0u\n#define OP_CLEAR_LAST 1u\n#define OP_DOWNSWEEP 2u\n\nlayout(local_size_x = THREADS_NUM, local_size_y = 1, local_size_z = 1) in;\n\nlayout(std430, binding = 0) buffer ssbo_local_offsets_buf { uint b_local_offsets_buf[]; }; // b_count_buf[THREAD_BLOCKS_NUM * BITSET_SIZE]\n\nuniform uint u_arr_len; // Already guaranteed to be a power of 2\nuniform uint u_depth;\nuniform uint u_op;\n\nuint to_partition_radixes_offsets_idx(uint radix, uint thread_block_idx)\n{\n return radix * u_arr_len + thread_block_idx;\n}\n\nuint to_loc_idx(uint item_idx, uint thread_idx)\n{\n return (thread_idx * ITEMS_NUM + item_idx);\n}\n\nuint to_key_idx(uint item_idx, uint thread_idx, uint thread_block_idx)\n{\n return (thread_block_idx * ITEMS_NUM * uint(THREADS_NUM)) + (thread_idx * ITEMS_NUM) + item_idx;\n}\n\nvoid main()\n{\n if (uint(fract(log2(float(u_arr_len)))) != 0u) {\n return; // ERROR: The u_arr_len must be a power of 2 otherwise the Blelloch scan won't work!\n }\n\n // ------------------------------------------------------------------------------------------------\n // Blelloch scan\n // ------------------------------------------------------------------------------------------------\n\n uint step = uint(exp2(float(u_depth)));\n\n if (u_op == OP_UPSWEEP)\n {\n // Reduce (upsweep)\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\n if (key_idx % (step * 2u) == 0u)\n {\n uint from_idx = key_idx + (step - 1u);\n uint to_idx = from_idx + step;\n\n if (to_idx < u_arr_len)\n {\n for (uint rad = 0u; rad < BITSET_SIZE; rad++)\n {\n uint from_rad_idx = to_partition_radixes_offsets_idx(rad, from_idx);\n uint to_rad_idx = to_partition_radixes_offsets_idx(rad, to_idx);\n\n b_local_offsets_buf[to_rad_idx] = b_local_offsets_buf[from_rad_idx] + b_local_offsets_buf[to_rad_idx];\n }\n }\n }\n }\n }\n else if (u_op == OP_DOWNSWEEP)\n {\n // Downsweep\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\n if (key_idx % (step * 2u) == 0u)\n {\n uint from_idx = key_idx + (step - 1u);\n uint to_idx = from_idx + step;\n\n if (to_idx < u_arr_len)\n {\n for (uint rad = 0u; rad < BITSET_SIZE; rad++)\n {\n uint from_rad_idx = to_partition_radixes_offsets_idx(rad, from_idx);\n uint to_rad_idx = to_partition_radixes_offsets_idx(rad, to_idx);\n\n uint r = b_local_offsets_buf[to_rad_idx];\n b_local_offsets_buf[to_rad_idx] = b_local_offsets_buf[from_rad_idx] + b_local_offsets_buf[to_rad_idx];\n b_local_offsets_buf[from_rad_idx] = r;\n }\n }\n }\n }\n }\n else// if (u_op == OP_CLEAR_LAST)\n {\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\n if (key_idx == (u_arr_len - 1u))\n {\n for (uint rad = 0u; rad < BITSET_SIZE; rad++)\n {\n uint idx = to_partition_radixes_offsets_idx(rad, key_idx);\n b_local_offsets_buf[idx] = 0u;\n }\n }\n }\n }\n}\n"; inline const char* __rgc_shader_injector_shader_src_8c254d92f3f4855a006b5a5c319997b2 = "\n#define THREAD_IDX gl_LocalInvocationIndex\n#define THREADS_NUM 64\n#define THREAD_BLOCK_IDX (gl_WorkGroupID.x + gl_NumWorkGroups.x * (gl_WorkGroupID.y + gl_NumWorkGroups.z * gl_WorkGroupID.z))\n#define THREAD_BLOCKS_NUM (gl_NumWorkGroups.x * gl_NumWorkGroups.y * gl_NumWorkGroups.z)\n#define ITEMS_NUM 4u\n#define BITSET_NUM 4u\n#define BITSET_SIZE 16u\n\n#define UINT32_MAX uint(-1)\n\nlayout(local_size_x = THREADS_NUM, local_size_y = 1, local_size_z = 1) in;\n\nlayout(std430, binding = 0) restrict readonly buffer in_keys_buf\n{\n uint b_in_keys[];\n};\n\nlayout(std430, binding = 1) restrict writeonly buffer out_keys_buf\n{\n uint b_out_keys[];\n};\n\nlayout(std430, binding = 2) restrict readonly buffer in_values_buf\n{\n uint b_in_values[];\n};\n\nlayout(std430, binding = 3) restrict writeonly buffer out_values_buf\n{\n uint b_out_values[];\n};\n\nlayout(std430, binding = 4) restrict readonly buffer local_offsets_buf\n{\n uint b_local_offsets_buf[];\n};\n\nlayout(std430, binding = 5) restrict readonly buffer global_counts_buf\n{\n uint b_glob_counts_buf[BITSET_SIZE];\n};\n\nuniform uint u_arr_len;\nuniform uint u_bitset_idx;\nuniform uint u_write_values;\n\nshared uint s_prefix_sum[BITSET_SIZE][uint(THREADS_NUM) * ITEMS_NUM];\nshared uint s_key_buf[uint(THREADS_NUM) * ITEMS_NUM][2];\nshared uint s_sorted_indices[uint(THREADS_NUM) * ITEMS_NUM][2];\nshared uint s_count[BITSET_SIZE];\n\nuint to_partition_radixes_offsets_idx(uint radix, uint thread_block_idx)\n{\n uint pow_of_2_thread_blocks_num = uint(exp2(ceil(log2(float(THREAD_BLOCKS_NUM)))));\n return radix * pow_of_2_thread_blocks_num + thread_block_idx;\n}\n\nuint to_loc_idx(uint item_idx, uint thread_idx)\n{\n return (thread_idx * ITEMS_NUM + item_idx);\n}\n\nuint to_key_idx(uint item_idx, uint thread_idx, uint thread_block_idx)\n{\n return (thread_block_idx * ITEMS_NUM * uint(THREADS_NUM)) + (thread_idx * ITEMS_NUM) + item_idx;\n}\n\nvoid main()\n{\n // ------------------------------------------------------------------------------------------------\n // Global offsets\n // ------------------------------------------------------------------------------------------------\n\n // Runs an exclusive scan on the global counts to get the starting offset for each radix.\n // The offsets are global for the whole input array.\n\n uint glob_off_buf[BITSET_SIZE];\n\n // Exclusive scan\n for (uint sum = 0u, i = 0u; i < BITSET_SIZE; i++)\n {\n glob_off_buf[i] = sum;\n sum += b_glob_counts_buf[i];\n }\n\n // ------------------------------------------------------------------------------------------------\n // Radix sort on partition\n // ------------------------------------------------------------------------------------------------\n\n // NOTE:\n // The last partition potentially can have keys that aren't part of the original array.\n // These keys during the radix sort are set to `UINT32_MAX` (or 0xFFFFFFFF), this way, after the array is sorted\n // they'll always be in the last position and won't be confused with the original keys of the input array.\n\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\n uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\n\n s_key_buf[loc_idx][0] = key_idx < u_arr_len ? b_in_keys[key_idx] : UINT32_MAX; // If the key_idx is outside of the input array then use UINT32_MAX.\n s_key_buf[loc_idx][1] = UINT32_MAX;\n\n s_sorted_indices[loc_idx][0] = loc_idx;\n s_sorted_indices[loc_idx][1] = UINT32_MAX;\n }\n\n barrier();\n\n // The offsets of the radixes within the partition. The values of this array will be written every radix-sort iteration.\n uint in_partition_group_off[BITSET_SIZE];\n\n uint bitset_idx;\n for (bitset_idx = 0u; bitset_idx <= u_bitset_idx; bitset_idx++)\n {\n uint bitset_mask = (BITSET_SIZE - 1u) << (BITSET_NUM * bitset_idx);\n\n // Init\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n for (uint bitset_val = 0u; bitset_val < BITSET_SIZE; bitset_val++)\n {\n uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\n s_prefix_sum[bitset_val][loc_idx] = 0u;\n }\n }\n\n barrier();\n\n // ------------------------------------------------------------------------------------------------\n // Predicate test\n // ------------------------------------------------------------------------------------------------\n\n // For each key of the current partition sets a 1 whether the radix corresponds to the radix of the current key.\n // Otherwise the default value is initialized to 0.\n\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\n uint k = s_key_buf[loc_idx][bitset_idx % 2u];\n uint radix = (k & bitset_mask) >> (BITSET_NUM * bitset_idx);\n s_prefix_sum[radix][loc_idx] = 1u;\n }\n\n barrier();\n\n // ------------------------------------------------------------------------------------------------\n // Exclusive sum\n // ------------------------------------------------------------------------------------------------\n\n // THREADS_NUM * ITEMS_NUM are guaranteed to be a power of two, otherwise it won't work!\n\n // An exclusive sum is run on the predicates, this way, for each location in the partition\n // we know the offset the element has to go, relative to its radix.\n\n // Up-sweep\n for (uint d = 0u; d < uint(log2(float(uint(THREADS_NUM) * ITEMS_NUM))); d++)\n {\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n uint step = uint(exp2(float(d)));\n uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\n\n if (loc_idx % (step * 2u) == 0u)\n {\n uint from_idx = loc_idx + (step - 1u);\n uint to_idx = from_idx + step;\n\n if (to_idx < uint(THREADS_NUM) * ITEMS_NUM)\n {\n for (uint bitset_val = 0u; bitset_val < BITSET_SIZE; bitset_val++)\n {\n s_prefix_sum[bitset_val][to_idx] = s_prefix_sum[bitset_val][from_idx] + s_prefix_sum[bitset_val][to_idx];\n }\n }\n }\n }\n\n barrier();\n }\n\n // Clear last\n if (THREAD_IDX == 0u)\n {\n for (uint bitset_val = 0u; bitset_val < BITSET_SIZE; bitset_val++)\n {\n s_prefix_sum[bitset_val][(uint(THREADS_NUM) * ITEMS_NUM) - 1u] = 0u;\n }\n }\n\n barrier();\n\n // Down-sweep\n for (int d = int(log2(float(uint(THREADS_NUM) * ITEMS_NUM))) - 1; d >= 0; d--)\n {\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n uint step = uint(exp2(float(d)));\n uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\n\n if (loc_idx % (step * 2u) == 0u)\n {\n uint from_idx = loc_idx + (step - 1u);\n uint to_idx = from_idx + step;\n\n if (to_idx < uint(THREADS_NUM) * ITEMS_NUM)\n {\n for (uint bitset_val = 0u; bitset_val < BITSET_SIZE; bitset_val++)\n {\n uint r = s_prefix_sum[bitset_val][to_idx];\n s_prefix_sum[bitset_val][to_idx] = r + s_prefix_sum[bitset_val][from_idx];\n s_prefix_sum[bitset_val][from_idx] = r;\n }\n }\n }\n }\n\n barrier();\n }\n\n // ------------------------------------------------------------------------------------------------\n // Shuffling\n // ------------------------------------------------------------------------------------------------\n\n uint last_loc_idx;\n if (THREAD_BLOCK_IDX == (THREAD_BLOCKS_NUM - 1u)) { // The last partition may be larger than the original size of the array, so the last position is before its end.\n last_loc_idx = u_arr_len - (THREAD_BLOCKS_NUM - 1u) * (uint(THREADS_NUM) * ITEMS_NUM) - 1u;\n } else {\n last_loc_idx = (uint(THREADS_NUM) * ITEMS_NUM) - 1u;\n }\n\n // Now for every radix we need to know its 'global' offset within the partition (we only know relative offsets per location within the same radix).\n // So we just issue an exclusive scan (again) on the last element offset of every radix.\n\n for (uint sum = 0u, i = 0u; i < BITSET_SIZE; i++) // Small prefix-sum to calculate group offsets.\n {\n in_partition_group_off[i] = sum;\n\n // Since we need the count of every bit-group, if the last element is the bitset value itself, we need to count it.\n // Otherwise it will be already counted.\n bool is_last = ((s_key_buf[last_loc_idx][bitset_idx % 2u] & bitset_mask) >> (BITSET_NUM * bitset_idx)) == i;\n sum += s_prefix_sum[i][last_loc_idx] + (is_last ? 1u : 0u);\n }\n\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\n uint k = s_key_buf[loc_idx][bitset_idx % 2u];\n uint radix = (k & bitset_mask) >> (BITSET_NUM * bitset_idx);\n\n // The destination address is calculated as the global offset of the radix plus the\n // local offset that depends by the location of the element.\n uint dest_addr = in_partition_group_off[radix] + s_prefix_sum[radix][loc_idx];\n s_key_buf[dest_addr][(bitset_idx + 1u) % 2u] = k; // Double-buffering is used every cycle the read and write buffer are swapped.\n s_sorted_indices[dest_addr][(bitset_idx + 1u) % 2u] = s_sorted_indices[loc_idx][bitset_idx % 2u];\n }\n\n barrier();\n }\n\n // TODO (REMOVE) WRITE s_prefix_sum\n /*\n for (uint item_idx = 0; item_idx < ITEMS_NUM; item_idx++)\n {\n uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\n if (item_idx < u_arr_len)\n {\n uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\n b_out_keys[key_idx] = s_prefix_sum[1][loc_idx];\n }\n }\n\n for (uint item_idx = 0; item_idx < ITEMS_NUM; item_idx++)\n {\n uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\n if (key_idx < u_arr_len)\n {\n uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\n b_out_keys[key_idx] = s_key_buf[loc_idx][bitset_idx % 2];\n }\n }\n */\n\n // ------------------------------------------------------------------------------------------------\n // Scattered writes to sorted partitions\n // ------------------------------------------------------------------------------------------------\n\n uint bitset_mask = (BITSET_SIZE - 1u) << (BITSET_NUM * u_bitset_idx);\n\n /*\n if (THREAD_IDX == 0)\n {\n for (uint i = 0; i < BITSET_SIZE; i++) {\n s_count[i] = 0;\n }\n }\n\n barrier();\n\n for (uint item_idx = 0; item_idx < ITEMS_NUM; item_idx++)\n {\n uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\n if (key_idx < u_arr_len)\n {\n uint k = s_key_buf[to_loc_idx(item_idx, THREAD_IDX)][bitset_idx % 2];\n uint rad = (k & bitset_mask) >> (BITSET_NUM * u_bitset_idx);\n\n atomicAdd(s_count[rad], 1);\n }\n }\n\n barrier();\n\n for (uint item_idx = 0; item_idx < ITEMS_NUM; item_idx++)\n {\n uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\n if (key_idx < u_arr_len)\n {\n uint k = s_key_buf[to_loc_idx(item_idx, THREAD_IDX)][bitset_idx % 2];\n uint rad = (k & bitset_mask) >> (BITSET_NUM * u_bitset_idx);\n\n b_out_keys[key_idx] = s_count[rad];\n }\n }\n\n barrier();\n */\n\n for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\n {\n uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\n if (key_idx < u_arr_len)\n {\n uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\n uint k = s_key_buf[loc_idx][bitset_idx % 2u];\n uint rad = (k & bitset_mask) >> (BITSET_NUM * u_bitset_idx);\n\n uint glob_off = glob_off_buf[rad];\n uint local_off = b_local_offsets_buf[to_partition_radixes_offsets_idx(rad, THREAD_BLOCK_IDX)];\n\n uint dest_idx = glob_off + local_off + (loc_idx - in_partition_group_off[rad]);\n\n b_out_keys[dest_idx] = k;\n if (u_write_values != 0u)\n {\n b_out_values[dest_idx] = b_in_values[THREAD_BLOCK_IDX * (uint(THREADS_NUM) * ITEMS_NUM) + s_sorted_indices[loc_idx][bitset_idx % 2u]];\n }\n }\n }\n}\n"; namespace rgc::radix_sort { inline const GLuint k_zero = 0; struct shader { GLuint m_name; shader(GLenum type) { m_name = glCreateShader(type); } ~shader() { glDeleteShader(m_name); } void src_from_txt(char const* txt) const { glShaderSource(m_name, 1, &txt, nullptr); } void src_from_txt_file(std::filesystem::path const& filename) const { std::ifstream file(filename); if (!file.is_open()) { throw std::invalid_argument("Failed to open text file."); } std::string src((std::istreambuf_iterator(file)), std::istreambuf_iterator()); src_from_txt(src.c_str()); } std::string get_info_log() const { GLint max_len = 0; glGetShaderiv(m_name, GL_INFO_LOG_LENGTH, &max_len); std::vector log(max_len); glGetShaderInfoLog(m_name, max_len, nullptr, log.data()); return std::string(log.begin(), log.end()); } void compile() const { glCompileShader(m_name); GLint status; glGetShaderiv(m_name, GL_COMPILE_STATUS, &status); if (status == GL_FALSE) { std::cerr << "Failed to compile:\n" << get_info_log() << std::endl; throw std::runtime_error("Couldn't compile shader. See console logs for more information."); } } }; struct program { GLuint m_name; program() { m_name = glCreateProgram(); } ~program() { glDeleteProgram(m_name); } void attach_shader(GLuint shader) const { glAttachShader(m_name, shader); } std::string get_info_log() const { GLint log_len = 0; glGetProgramiv(m_name, GL_INFO_LOG_LENGTH, &log_len); std::vector log(log_len); glGetProgramInfoLog(m_name, log_len, nullptr, log.data()); return std::string(log.begin(), log.end()); } void link() const { GLint status; glLinkProgram(m_name); glGetProgramiv(m_name, GL_LINK_STATUS, &status); if (status == GL_FALSE) { std::cerr << get_info_log() << std::endl; throw std::runtime_error("Couldn't link shader program."); } } GLint get_uniform_location(const char* name) const { GLint loc = glGetUniformLocation(m_name, name); if (loc < 0) { throw std::runtime_error("Couldn't find uniform. Is it unused maybe?"); } return loc; } void use() const { glUseProgram(m_name); } static void unuse() { glUseProgram(0); } }; struct sorter { private: program m_count_program; program m_local_offsets_program; program m_reorder_program; size_t m_internal_arr_len; GLuint m_local_offsets_buf; GLuint m_keys_scratch_buf; GLuint m_values_scratch_buf; GLuint m_glob_counts_buf; GLuint calc_thread_blocks_num(size_t arr_len) { return GLuint(ceil(float(arr_len) / float(RGC_RADIX_SORT_THREADS_PER_BLOCK * RGC_RADIX_SORT_ITEMS_PER_THREAD))); } template T round_to_power_of_2(T dim) { return (T) exp2(ceil(log2(dim))); } void resize_internal_buf(size_t arr_len) { if (m_local_offsets_buf != 0) glDeleteBuffers(1, &m_local_offsets_buf); if (m_glob_counts_buf != 0) glDeleteBuffers(1, &m_glob_counts_buf); if (m_keys_scratch_buf != 0) glDeleteBuffers(1, &m_keys_scratch_buf); if (m_values_scratch_buf != 0) glDeleteBuffers(1, &m_values_scratch_buf); m_internal_arr_len = arr_len; glGenBuffers(1, &m_local_offsets_buf); // TODO TRY TO REMOVE THIS BUFFER glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_local_offsets_buf); glBufferStorage(GL_SHADER_STORAGE_BUFFER, GLsizeiptr(round_to_power_of_2(calc_thread_blocks_num(arr_len)) * RGC_RADIX_SORT_BITSET_SIZE * sizeof(GLuint)), nullptr, GL_DYNAMIC_STORAGE_BIT); glGenBuffers(1, &m_glob_counts_buf); glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_glob_counts_buf); // The spec says we don't need the GL_DYNAMIC_STORAGE_BIT. // but if we don't include it for Intel UHD graphics glClearBufferData will fail glBufferStorage(GL_SHADER_STORAGE_BUFFER, GLsizeiptr(RGC_RADIX_SORT_BITSET_SIZE * sizeof(GLuint)), nullptr, GL_DYNAMIC_STORAGE_BIT); glGenBuffers(1, &m_keys_scratch_buf); glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_keys_scratch_buf); glBufferStorage(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr) (arr_len * sizeof(GLuint)), nullptr, 0); glGenBuffers(1, &m_values_scratch_buf); glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_values_scratch_buf); glBufferStorage(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr) (arr_len * sizeof(GLuint)), nullptr, 0); } public: sorter(size_t init_arr_len) { { shader sh(GL_COMPUTE_SHADER); __rgc_shader_injector_load_src(sh.m_name, __rgc_shader_injector_shader_src_6213812085af7087c07246dabc62da60); sh.compile(); m_count_program.attach_shader(sh.m_name); m_count_program.link(); } { shader sh(GL_COMPUTE_SHADER); __rgc_shader_injector_load_src(sh.m_name, __rgc_shader_injector_shader_src_9992419d7253eb1e5310935c952c8eff); sh.compile(); m_local_offsets_program.attach_shader(sh.m_name); m_local_offsets_program.link(); } { shader sh(GL_COMPUTE_SHADER); __rgc_shader_injector_load_src(sh.m_name, __rgc_shader_injector_shader_src_8c254d92f3f4855a006b5a5c319997b2); sh.compile(); m_reorder_program.attach_shader(sh.m_name); m_reorder_program.link(); } resize_internal_buf(init_arr_len); } ~sorter() { if (m_local_offsets_buf != 0) glDeleteBuffers(1, &m_local_offsets_buf); if (m_glob_counts_buf != 0) glDeleteBuffers(1, &m_glob_counts_buf); if (m_keys_scratch_buf != 0) glDeleteBuffers(1, &m_keys_scratch_buf); } void sort(GLuint key_buf, GLuint val_buf, size_t arr_len) { if (arr_len <= 1) { return; } if (m_internal_arr_len < arr_len) { resize_internal_buf(arr_len); } // ------------------------------------------------------------------------------------------------ // Sorting // ------------------------------------------------------------------------------------------------ GLuint thread_blocks_num = calc_thread_blocks_num(arr_len); GLuint power_of_two_thread_blocks_num = round_to_power_of_2(thread_blocks_num); GLuint keys_buffers[2] = {key_buf, m_keys_scratch_buf}; GLuint values_buffers[2] = {val_buf, m_values_scratch_buf}; for (GLuint pass = 0; pass < RGC_RADIX_SORT_BITSET_COUNT; pass++) { // ------------------------------------------------------------------------------------------------ // Initial clearing // ------------------------------------------------------------------------------------------------ glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_glob_counts_buf); #ifdef __ANDROID__ size_t glob_counts_size = RGC_RADIX_SORT_BITSET_SIZE * sizeof(GLuint); ZeroBuffer(GL_SHADER_STORAGE_BUFFER, glob_counts_size); #else glClearBufferData(GL_SHADER_STORAGE_BUFFER, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, &k_zero); #endif glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_local_offsets_buf); #ifdef __ANDROID__ size_t local_offsets_size = round_to_power_of_2(calc_thread_blocks_num(arr_len)) * RGC_RADIX_SORT_BITSET_SIZE * sizeof(GLuint); ZeroBuffer(GL_SHADER_STORAGE_BUFFER, local_offsets_size); #else glClearBufferData(GL_SHADER_STORAGE_BUFFER, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, &k_zero); #endif // ------------------------------------------------------------------------------------------------ // Counting // ------------------------------------------------------------------------------------------------ // Per-block & global radix count m_count_program.use(); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, keys_buffers[pass % 2]); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_local_offsets_buf); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_glob_counts_buf); glUniform1ui(m_count_program.get_uniform_location("u_arr_len"), (GLuint)arr_len); glUniform1ui(m_count_program.get_uniform_location("u_bitset_idx"), pass); RGC_RADIX_SORT_RENDERDOC_WATCH(true, [&]() { glDispatchCompute(thread_blocks_num, 1, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); }); // ------------------------------------------------------------------------------------------------ // Local offsets (block-wide exclusive scan per radix) // ------------------------------------------------------------------------------------------------ m_local_offsets_program.use(); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_local_offsets_buf); // Up-sweep (reduction) for (GLuint d = 0; d < GLuint(log2(power_of_two_thread_blocks_num)); d++) { glUniform1ui(m_local_offsets_program.get_uniform_location("u_arr_len"), power_of_two_thread_blocks_num); glUniform1ui(m_local_offsets_program.get_uniform_location("u_op"), 0); glUniform1ui(m_local_offsets_program.get_uniform_location("u_depth"), d); RGC_RADIX_SORT_RENDERDOC_WATCH(false, [&]() { auto workgroups_num = GLuint(ceil(float(power_of_two_thread_blocks_num) / float(RGC_RADIX_SORT_THREADS_PER_BLOCK * RGC_RADIX_SORT_ITEMS_PER_THREAD))); glDispatchCompute(workgroups_num, 1, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); }); } // Clear last glUniform1ui(m_local_offsets_program.get_uniform_location("u_arr_len"), power_of_two_thread_blocks_num); glUniform1ui(m_local_offsets_program.get_uniform_location("u_op"), 1); RGC_RADIX_SORT_RENDERDOC_WATCH(false, [&]() { auto workgroups_num = GLuint(ceil(float(power_of_two_thread_blocks_num) / float(RGC_RADIX_SORT_THREADS_PER_BLOCK * RGC_RADIX_SORT_ITEMS_PER_THREAD))); glDispatchCompute(workgroups_num, 1, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); }); // Down-sweep for (GLint d = GLint(log2(power_of_two_thread_blocks_num)) - 1; d >= 0; d--) { glUniform1ui(m_local_offsets_program.get_uniform_location("u_arr_len"), power_of_two_thread_blocks_num); glUniform1ui(m_local_offsets_program.get_uniform_location("u_op"), 2); glUniform1ui(m_local_offsets_program.get_uniform_location("u_depth"), d); RGC_RADIX_SORT_RENDERDOC_WATCH(false, [&]() { auto workgroups_num = GLuint(ceil(float(power_of_two_thread_blocks_num) / float(RGC_RADIX_SORT_THREADS_PER_BLOCK * RGC_RADIX_SORT_ITEMS_PER_THREAD))); glDispatchCompute(workgroups_num, 1, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); }); } // ------------------------------------------------------------------------------------------------ // Reordering // ------------------------------------------------------------------------------------------------ // In thread-block reordering & writes to global memory m_reorder_program.use(); glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, keys_buffers[pass % 2], 0, (GLsizeiptr) (arr_len * sizeof(GLuint))); glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, keys_buffers[(pass + 1) % 2], 0, (GLsizeiptr) (arr_len * sizeof(GLuint))); if (val_buf != 0) { glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, values_buffers[pass % 2], 0, (GLsizeiptr) (arr_len * sizeof(GLuint))); glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 3, values_buffers[(pass + 1) % 2], 0, (GLsizeiptr) (arr_len * sizeof(GLuint))); } glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, m_local_offsets_buf); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, m_glob_counts_buf); glUniform1ui(m_reorder_program.get_uniform_location("u_write_values"), val_buf != 0); glUniform1ui(m_reorder_program.get_uniform_location("u_arr_len"), (GLuint)arr_len); glUniform1ui(m_reorder_program.get_uniform_location("u_bitset_idx"), pass); RGC_RADIX_SORT_RENDERDOC_WATCH(true, [&]() { glDispatchCompute(thread_blocks_num, 1, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); }); } program::unuse(); } }; } ================================================ FILE: src/sdl_main.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ // 3d gaussian splat renderer #include #include #include #include #include #include #include #include #ifdef TRACY_ENABLE #include #else #define FrameMark do {} while(0) #endif #include "core/log.h" #include "core/util.h" #include "app.h" //#define SOFTWARE_SPLATS struct GlobalContext { bool quitting = false; SDL_Window* window = NULL; SDL_GLContext gl_context; }; GlobalContext ctx; int SDLCALL Watch(void *userdata, SDL_Event* event) { if (event->type == SDL_APP_WILLENTERBACKGROUND) { ctx.quitting = true; } return 1; } int main(int argc, char *argv[]) { Log::SetAppName("splataplut"); MainContext mainContext; App app(mainContext); App::ParseResult parseResult = app.ParseArguments(argc, (const char**)argv); switch (parseResult) { case App::SUCCESS_RESULT: break; case App::ERROR_RESULT: Log::E("App::ParseArguments failed!\n"); return 1; case App::QUIT_RESULT: return 0; } if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_JOYSTICK) != 0) { Log::E("Failed to initialize SDL: %s\n", SDL_GetError()); return 1; } const int32_t WIDTH = 1024; const int32_t HEIGHT = 768; // Allow us to use automatic linear->sRGB conversion. SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, 1); // increase depth buffer size SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); uint32_t windowFlags = SDL_WINDOW_OPENGL; if (app.IsFullscreen()) { windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP; } else { windowFlags |= SDL_WINDOW_RESIZABLE; } ctx.window = SDL_CreateWindow("splatapult", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WIDTH, HEIGHT, windowFlags); if (!ctx.window) { Log::E("Failed to create window: %s\n", SDL_GetError()); return 1; } ctx.gl_context = SDL_GL_CreateContext(ctx.window); SDL_GL_MakeCurrent(ctx.window, ctx.gl_context); #ifdef __linux__ // Initialize context from the SDL window SDL_SysWMinfo info; SDL_VERSION(&info.version) auto ret = SDL_GetWindowWMInfo(ctx.window, &info); if (ret != SDL_TRUE) { Log::W("Failed to retrieve SDL window info: %s\n", SDL_GetError()); } else { mainContext.xdisplay = info.info.x11.display; mainContext.glxDrawable = (GLXWindow)info.info.x11.window; mainContext.glxContext = (GLXContext)ctx.gl_context; } #endif GLenum err = glewInit(); if (GLEW_OK != err) { Log::E("Error: %s\n", glewGetErrorString(err)); return 1; } // AJT: TODO REMOVE disable vsync for benchmarks SDL_GL_SetSwapInterval(0); SDL_AddEventWatch(Watch, NULL); if (!app.Init()) { Log::E("App::Init failed\n"); return 1; } bool shouldQuit = false; app.OnQuit([&shouldQuit]() { shouldQuit = true; }); app.OnResize([](int newWidth, int newHeight) { //SDL_RenderSetLogicalSize(ctx.renderer, newWidth, newHeight); // AJT: TODO resize texture? }); uint32_t frameCount = 1; uint32_t frameTicks = SDL_GetTicks(); uint32_t lastTicks = SDL_GetTicks(); while (!ctx.quitting && !shouldQuit) { // update dt uint32_t ticks = SDL_GetTicks(); const int FPS_FRAMES = 100; if ((frameCount % FPS_FRAMES) == 0) { float delta = (ticks - frameTicks) / 1000.0f; float fps = (float)FPS_FRAMES / delta; frameTicks = ticks; app.UpdateFps(fps); } float dt = (ticks - lastTicks) / 1000.0f; lastTicks = ticks; SDL_Event event; while (SDL_PollEvent(&event)) { app.ProcessEvent(event); } if (!app.Process(dt)) { Log::E("App::Process failed!\n"); return 1; } SDL_GL_MakeCurrent(ctx.window, ctx.gl_context); int width, height; SDL_GetWindowSize(ctx.window, &width, &height); if (!app.Render(dt, glm::ivec2(width, height))) { Log::E("App::Render failed!\n"); return 1; } SDL_GL_SwapWindow(ctx.window); frameCount++; FrameMark; } SDL_DelEventWatch(Watch, NULL); SDL_GL_DeleteContext(ctx.gl_context); SDL_DestroyWindow(ctx.window); SDL_Quit(); return 0; } ================================================ FILE: src/splatrenderer.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "splatrenderer.h" #ifdef __ANDROID__ #include #include #include #include #else #include #endif #include #ifdef TRACY_ENABLE #include #else #define ZoneScoped #define ZoneScopedNC(NAME, COLOR) #endif #include "core/image.h" #include "core/log.h" #include "core/texture.h" #include "core/util.h" #include "radix_sort.hpp" static const uint32_t NUM_BLOCKS_PER_WORKGROUP = 1024; static void SetupAttrib(int loc, const BinaryAttribute& attrib, int32_t count, size_t stride) { assert(attrib.type == BinaryAttribute::Type::Float); glVertexAttribPointer(loc, count, GL_FLOAT, GL_FALSE, (uint32_t)stride, (void*)attrib.offset); glEnableVertexAttribArray(loc); } SplatRenderer::SplatRenderer() { } SplatRenderer::~SplatRenderer() { } bool SplatRenderer::Init(std::shared_ptr gaussianCloud, bool isFramebufferSRGBEnabledIn, bool useRgcSortOverrideIn) { ZoneScopedNC("SplatRenderer::Init()", tracy::Color::Blue); GL_ERROR_CHECK("SplatRenderer::Init() begin"); isFramebufferSRGBEnabled = isFramebufferSRGBEnabledIn; useRgcSortOverride = useRgcSortOverrideIn; splatProg = std::make_shared(); if (isFramebufferSRGBEnabled || gaussianCloud->HasFullSH()) { std::string defines = ""; if (isFramebufferSRGBEnabled) { defines += "#define FRAMEBUFFER_SRGB\n"; } if (gaussianCloud->HasFullSH()) { defines += "#define FULL_SH\n"; } splatProg->AddMacro("DEFINES", defines); } if (!splatProg->LoadVertGeomFrag("shader/splat_vert.glsl", "shader/splat_geom.glsl", "shader/splat_frag.glsl")) { Log::E("Error loading splat shaders!\n"); return false; } preSortProg = std::make_shared(); if (!preSortProg->LoadCompute("shader/presort_compute.glsl")) { Log::E("Error loading pre-sort compute shader!\n"); return false; } bool useMultiRadixSort = GLEW_KHR_shader_subgroup && !useRgcSortOverride; if (useMultiRadixSort) { sortProg = std::make_shared(); if (!sortProg->LoadCompute("shader/multi_radixsort.glsl")) { Log::E("Error loading sort compute shader!\n"); return false; } histogramProg = std::make_shared(); if (!histogramProg->LoadCompute("shader/multi_radixsort_histograms.glsl")) { Log::E("Error loading histogram compute shader!\n"); return false; } } // build posVec size_t numGaussians = gaussianCloud->GetNumGaussians(); posVec.reserve(numGaussians); gaussianCloud->ForEachPosWithAlpha([this](const float* pos) { posVec.emplace_back(glm::vec4(pos[0], pos[1], pos[2], 1.0f)); }); BuildVertexArrayObject(gaussianCloud); depthVec.resize(numGaussians); if (useMultiRadixSort) { Log::I("using multi_radixsort.glsl\n"); keyBuffer = std::make_shared(GL_SHADER_STORAGE_BUFFER, depthVec, GL_DYNAMIC_STORAGE_BIT); keyBuffer2 = std::make_shared(GL_SHADER_STORAGE_BUFFER, depthVec, GL_DYNAMIC_STORAGE_BIT); const uint32_t NUM_ELEMENTS = static_cast(numGaussians); const uint32_t NUM_WORKGROUPS = (NUM_ELEMENTS + numBlocksPerWorkgroup - 1) / numBlocksPerWorkgroup; const uint32_t RADIX_SORT_BINS = 256; std::vector histogramVec(NUM_WORKGROUPS * RADIX_SORT_BINS, 0); histogramBuffer = std::make_shared(GL_SHADER_STORAGE_BUFFER, histogramVec, GL_DYNAMIC_STORAGE_BIT); valBuffer = std::make_shared(GL_SHADER_STORAGE_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT); valBuffer2 = std::make_shared(GL_SHADER_STORAGE_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT); posBuffer = std::make_shared(GL_SHADER_STORAGE_BUFFER, posVec); } else { Log::I("using rgc::radix_sort\n"); keyBuffer = std::make_shared(GL_SHADER_STORAGE_BUFFER, depthVec, GL_DYNAMIC_STORAGE_BIT); valBuffer = std::make_shared(GL_SHADER_STORAGE_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT); posBuffer = std::make_shared(GL_SHADER_STORAGE_BUFFER, posVec); sorter = std::make_shared(numGaussians); } atomicCounterVec.resize(1, 0); atomicCounterBuffer = std::make_shared(GL_ATOMIC_COUNTER_BUFFER, atomicCounterVec, GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT); GL_ERROR_CHECK("SplatRenderer::Init() end"); return true; } void SplatRenderer::Sort(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar) { ZoneScoped; GL_ERROR_CHECK("SplatRenderer::Sort() begin"); const size_t numPoints = posVec.size(); glm::mat4 modelViewMat = glm::inverse(cameraMat); bool useMultiRadixSort = GLEW_KHR_shader_subgroup && !useRgcSortOverride; // 24 bit radix sort still has some artifacts on some datasets, so use 32 bit sort. //const uint32_t NUM_BYTES = useMultiRadixSort ? 3 : 4; //const uint32_t MAX_DEPTH = useMultiRadixSort ? 16777215 : std::numeric_limits::max(); const uint32_t NUM_BYTES = 4; const uint32_t MAX_DEPTH = std::numeric_limits::max(); { ZoneScopedNC("pre-sort", tracy::Color::Red4); preSortProg->Bind(); preSortProg->SetUniform("modelViewProj", projMat * modelViewMat); preSortProg->SetUniform("nearFar", nearFar); preSortProg->SetUniform("keyMax", MAX_DEPTH); // reset counter back to zero atomicCounterVec[0] = 0; atomicCounterBuffer->Update(atomicCounterVec); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, posBuffer->GetObj()); // readonly glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, keyBuffer->GetObj()); // writeonly glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, valBuffer->GetObj()); // writeonly glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 4, atomicCounterBuffer->GetObj()); const int LOCAL_SIZE = 256; glDispatchCompute(((GLuint)numPoints + (LOCAL_SIZE - 1)) / LOCAL_SIZE, 1, 1); // Assuming LOCAL_SIZE threads per group glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT); GL_ERROR_CHECK("SplatRenderer::Sort() pre-sort"); } { ZoneScopedNC("get-count", tracy::Color::Green); atomicCounterBuffer->Read(atomicCounterVec); sortCount = atomicCounterVec[0]; assert(sortCount <= (uint32_t)numPoints); GL_ERROR_CHECK("SplatRenderer::Render() get-count"); } if (useMultiRadixSort) { ZoneScopedNC("sort", tracy::Color::Red4); const uint32_t NUM_ELEMENTS = static_cast(sortCount); const uint32_t NUM_WORKGROUPS = (NUM_ELEMENTS + numBlocksPerWorkgroup - 1) / numBlocksPerWorkgroup; sortProg->Bind(); sortProg->SetUniform("g_num_elements", NUM_ELEMENTS); sortProg->SetUniform("g_num_workgroups", NUM_WORKGROUPS); sortProg->SetUniform("g_num_blocks_per_workgroup", numBlocksPerWorkgroup); histogramProg->Bind(); histogramProg->SetUniform("g_num_elements", NUM_ELEMENTS); //histogramProg->SetUniform("g_num_workgroups", NUM_WORKGROUPS); histogramProg->SetUniform("g_num_blocks_per_workgroup", numBlocksPerWorkgroup); for (uint32_t i = 0; i < NUM_BYTES; i++) { histogramProg->Bind(); histogramProg->SetUniform("g_shift", 8 * i); if (i == 0 || i == 2) { glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, keyBuffer->GetObj()); } else { glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, keyBuffer2->GetObj()); } glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, histogramBuffer->GetObj()); glDispatchCompute(NUM_WORKGROUPS, 1, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); sortProg->Bind(); sortProg->SetUniform("g_shift", 8 * i); if ((i % 2) == 0) // even { glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, keyBuffer->GetObj()); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, keyBuffer2->GetObj()); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, valBuffer->GetObj()); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, valBuffer2->GetObj()); } else // odd { glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, keyBuffer2->GetObj()); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, keyBuffer->GetObj()); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, valBuffer2->GetObj()); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, valBuffer->GetObj()); } glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, histogramBuffer->GetObj()); glDispatchCompute(NUM_WORKGROUPS, 1, 1); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); } GL_ERROR_CHECK("SplatRenderer::Sort() sort"); // indicate if keys are sorted properly or not. if (false) { std::vector sortedKeyVec(numPoints, 0); keyBuffer->Read(sortedKeyVec); GL_ERROR_CHECK("SplatRenderer::Sort() READ buffer"); bool sorted = true; for (uint32_t i = 1; i < sortCount; i++) { if (sortedKeyVec[i - 1] > sortedKeyVec[i]) { sorted = false; break; } } printf("%s", sorted ? "o" : "x"); } } else { ZoneScopedNC("sort", tracy::Color::Red4); sorter->sort(keyBuffer->GetObj(), valBuffer->GetObj(), sortCount); GL_ERROR_CHECK("SplatRenderer::Sort() rgc sort"); } { ZoneScopedNC("copy-sorted", tracy::Color::DarkGreen); if (useMultiRadixSort && (NUM_BYTES % 2) == 1) // odd { glBindBuffer(GL_COPY_READ_BUFFER, valBuffer2->GetObj()); } else { glBindBuffer(GL_COPY_READ_BUFFER, valBuffer->GetObj()); } glBindBuffer(GL_COPY_WRITE_BUFFER, splatVao->GetElementBuffer()->GetObj()); glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sortCount * sizeof(uint32_t)); GL_ERROR_CHECK("SplatRenderer::Sort() copy-sorted"); } } void SplatRenderer::Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar) { ZoneScoped; GL_ERROR_CHECK("SplatRenderer::Render() begin"); { ZoneScopedNC("draw", tracy::Color::Red4); float width = viewport.z; float height = viewport.w; float aspectRatio = width / height; glm::mat4 viewMat = glm::inverse(cameraMat); glm::vec3 eye = glm::vec3(cameraMat[3]); splatProg->Bind(); splatProg->SetUniform("viewMat", viewMat); splatProg->SetUniform("projMat", projMat); splatProg->SetUniform("viewport", viewport); splatProg->SetUniform("projParams", glm::vec4(0.0f, nearFar.x, nearFar.y, 0.0f)); splatProg->SetUniform("eye", eye); splatVao->Bind(); glDrawElements(GL_POINTS, sortCount, GL_UNSIGNED_INT, nullptr); splatVao->Unbind(); GL_ERROR_CHECK("SplatRenderer::Render() draw"); } } void SplatRenderer::BuildVertexArrayObject(std::shared_ptr gaussianCloud) { splatVao = std::make_shared(); // allocate large buffer to hold interleaved vertex data gaussianDataBuffer = std::make_shared(GL_ARRAY_BUFFER, gaussianCloud->GetRawDataPtr(), gaussianCloud->GetTotalSize(), 0); const size_t numGaussians = gaussianCloud->GetNumGaussians(); // build element array indexVec.reserve(numGaussians); assert(numGaussians <= std::numeric_limits::max()); for (uint32_t i = 0; i < (uint32_t)numGaussians; i++) { indexVec.push_back(i); } auto indexBuffer = std::make_shared(GL_ELEMENT_ARRAY_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT); splatVao->Bind(); gaussianDataBuffer->Bind(); const size_t stride = gaussianCloud->GetStride(); SetupAttrib(splatProg->GetAttribLoc("position"), gaussianCloud->GetPosWithAlphaAttrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("r_sh0"), gaussianCloud->GetR_SH0Attrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("g_sh0"), gaussianCloud->GetG_SH0Attrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("b_sh0"), gaussianCloud->GetB_SH0Attrib(), 4, stride); if (gaussianCloud->HasFullSH()) { SetupAttrib(splatProg->GetAttribLoc("r_sh1"), gaussianCloud->GetR_SH1Attrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("r_sh2"), gaussianCloud->GetR_SH2Attrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("r_sh3"), gaussianCloud->GetR_SH3Attrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("g_sh1"), gaussianCloud->GetG_SH1Attrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("g_sh2"), gaussianCloud->GetG_SH2Attrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("g_sh3"), gaussianCloud->GetG_SH3Attrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("b_sh1"), gaussianCloud->GetB_SH1Attrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("b_sh2"), gaussianCloud->GetB_SH2Attrib(), 4, stride); SetupAttrib(splatProg->GetAttribLoc("b_sh3"), gaussianCloud->GetB_SH3Attrib(), 4, stride); } SetupAttrib(splatProg->GetAttribLoc("cov3_col0"), gaussianCloud->GetCov3_Col0Attrib(), 3, stride); SetupAttrib(splatProg->GetAttribLoc("cov3_col1"), gaussianCloud->GetCov3_Col1Attrib(), 3, stride); SetupAttrib(splatProg->GetAttribLoc("cov3_col2"), gaussianCloud->GetCov3_Col2Attrib(), 3, stride); splatVao->SetElementBuffer(indexBuffer); gaussianDataBuffer->Unbind(); } ================================================ FILE: src/splatrenderer.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include #include #include "core/program.h" #include "core/vertexbuffer.h" #include "gaussiancloud.h" namespace rgc::radix_sort { struct sorter; } class SplatRenderer { public: SplatRenderer(); ~SplatRenderer(); bool Init(std::shared_ptr gaussianCloud, bool isFramebufferSRGBEnabledIn, bool useRgcSortOverrideIn); void Sort(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar); // viewport = (x, y, width, height) void Render(const glm::mat4& cameraMat, const glm::mat4& projMat, const glm::vec4& viewport, const glm::vec2& nearFar); public: uint32_t numBlocksPerWorkgroup = 1024; protected: void BuildVertexArrayObject(std::shared_ptr gaussianCloud); std::shared_ptr sorter; std::shared_ptr splatProg; std::shared_ptr preSortProg; std::shared_ptr histogramProg; std::shared_ptr sortProg; std::shared_ptr splatVao; std::vector indexVec; std::vector depthVec; std::vector posVec; std::vector atomicCounterVec; std::shared_ptr gaussianDataBuffer; std::shared_ptr keyBuffer; std::shared_ptr keyBuffer2; std::shared_ptr histogramBuffer; std::shared_ptr valBuffer; std::shared_ptr valBuffer2; std::shared_ptr posBuffer; std::shared_ptr atomicCounterBuffer; uint32_t sortCount; bool isFramebufferSRGBEnabled; bool useRgcSortOverride; }; ================================================ FILE: src/vrconfig.cpp ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #include "vrconfig.h" #include #include #include #include "core/log.h" #include "core/util.h" VrConfig::VrConfig() : floorMat(1.0f) { ; } bool VrConfig::ImportJson(const std::string& jsonFilename) { std::ifstream f(jsonFilename); if (f.fail()) { return false; } try { nlohmann::json obj = nlohmann::json::parse(f); nlohmann::json jmat = obj["floorMat"]; glm::mat4 mat(jmat[0][0].template get(), jmat[1][0].template get(), jmat[2][0].template get(), jmat[3][0].template get(), jmat[0][1].template get(), jmat[1][1].template get(), jmat[2][1].template get(), jmat[3][1].template get(), jmat[0][2].template get(), jmat[1][2].template get(), jmat[2][2].template get(), jmat[3][2].template get(), jmat[0][3].template get(), jmat[1][3].template get(), jmat[2][3].template get(), jmat[3][3].template get()); floorMat = mat; } catch (const nlohmann::json::exception& e) { std::string s = e.what(); Log::E("VrConfig::ImportJson exception: %s\n", s.c_str()); return false; } return true; } bool VrConfig::ExportJson(const std::string& jsonFilename) const { std::ofstream f(jsonFilename); if (f.fail()) { return false; } f << "{" << std::endl; f << " \"floorMat\": ["; f << "[" << floorMat[0][0] << ", " << floorMat[1][0] << ", " << floorMat[2][0] << ", " << floorMat[3][0] << "], "; f << "[" << floorMat[0][1] << ", " << floorMat[1][1] << ", " << floorMat[2][1] << ", " << floorMat[3][1] << "], "; f << "[" << floorMat[0][2] << ", " << floorMat[1][2] << ", " << floorMat[2][2] << ", " << floorMat[3][2] << "], "; f << "[" << floorMat[0][3] << ", " << floorMat[1][3] << ", " << floorMat[2][3] << ", " << floorMat[3][3] << "]]"; f << std::endl << "}"; return true; } ================================================ FILE: src/vrconfig.h ================================================ /* Copyright (c) 2024 Anthony J. Thibault This software is licensed under the MIT License. See LICENSE for more details. */ #pragma once #include #include #include class VrConfig { public: VrConfig(); bool ImportJson(const std::string& jsonFilename); bool ExportJson(const std::string& jsonFilename) const; const glm::mat4& GetFloorMat() const { return floorMat; } void SetFloorMat(const glm::mat4& floorMatIn) { floorMat = floorMatIn; } protected: glm::mat4 floorMat; }; ================================================ FILE: tasks.py ================================================ from invoke import task import os import shutil RELEASE_NAME = "splatapult-0.1-x64" @task def clean(c): c.run("rm -rf build") def build_with_config(c, config, options={}): if (not os.path.exists("build")): c.run("mkdir build") with c.cd("build"): defines = [] for k, v in options.items(): defines.append(f"-D{k}={v}") c.run(f'cmake {" ".join(defines)} -DCMAKE_TOOLCHAIN_FILE="../vcpkg/scripts/buildsystems/vcpkg.cmake" ..') c.run(f"cmake --build . --config={config}") @task def build(c): build_with_config(c, "Release", {"SHIPPING": "ON"}) @task def build_debug(c): build_with_config(c, "Debug") @task def archive(c): shutil.make_archive(RELEASE_NAME, "zip", "build/Release") @task def deploy(c): c.run(f"cp {RELEASE_NAME}.zip ../hyperlogic.github.io/files") with c.cd("../hyperlogic.github.io"): c.run(f"git add files/{RELEASE_NAME}.zip") c.run(f'git commit -m "Automated deploy of {RELEASE_NAME}"') c.run("git push") @task def all(c): clean(c) build(c) archive(c) deploy(c) ================================================ FILE: vcpkg.json ================================================ { "name": "main", "version-string": "latest", "dependencies": [ "sdl2", "glew", "glm", "libpng", "nlohmann-json", "eigen3", "tracy", "openxr-loader" ] }