├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-task.yaml │ └── maintenance-task.md ├── PULL_REQUEST_TEMPLATE │ └── pull-request-task.md └── workflows │ └── ci.yaml ├── .gitignore ├── .rustfmt.toml ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── ACKNOWLEDGEMENTS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benchmarks ├── README.md └── stress-test │ ├── Cargo.toml │ ├── README.md │ ├── logo.png │ ├── scripts │ ├── run_on_device.ps1 │ └── run_on_device.sh │ └── src │ ├── lib.rs │ ├── main.rs │ └── systems │ ├── cube.rs │ └── mod.rs ├── cspell.json ├── docs └── hotham_sync_loop.png ├── examples ├── common_lib │ └── arm64-v8a │ │ └── libopenxr_loader.so ├── complex-scene │ ├── Cargo.toml │ ├── README.md │ ├── openxr_loader.dll │ ├── scripts │ │ ├── run_on_device.ps1 │ │ └── run_on_device.sh │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── crab-saber │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── assets │ │ ├── Chasms - Dark Matter.mp3 │ │ ├── Hit.mp3 │ │ ├── Miss.mp3 │ │ ├── NEFFEX - Tell Me That I Can't.mp3 │ │ ├── Spence - Right Here Beside You.mp3 │ │ ├── TrackTribe - Cloud Echo.mp3 │ │ └── crab_saber.glb │ ├── openxr_loader.dll │ ├── scripts │ │ ├── run_on_device.ps1 │ │ └── run_on_device.sh │ └── src │ │ ├── components │ │ ├── color.rs │ │ ├── cube.rs │ │ ├── mod.rs │ │ └── saber.rs │ │ ├── game_context.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ └── systems │ │ ├── game.rs │ │ ├── mod.rs │ │ └── sabers.rs ├── custom-rendering │ ├── Cargo.toml │ ├── README.md │ ├── openxr_loader.dll │ ├── scripts │ │ ├── run_on_device.ps1 │ │ └── run_on_device.sh │ └── src │ │ ├── custom_render_context.rs │ │ ├── custom_rendering.rs │ │ ├── hologram.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── shaders │ │ ├── quadric.frag │ │ ├── quadric.glsl │ │ └── quadric.vert │ │ └── surface_solver.rs ├── hotham_examples.keystore ├── shared │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── navigation.rs └── simple-scene │ ├── Cargo.toml │ ├── README.md │ ├── logo.png │ ├── openxr_loader.dll │ ├── scripts │ ├── run_on_device.ps1 │ └── run_on_device.sh │ └── src │ ├── lib.rs │ └── main.rs ├── hotham-asset-client ├── Cargo.toml └── src │ ├── client.rs │ ├── lib.rs │ └── message.rs ├── hotham-asset-server ├── Cargo.toml └── src │ ├── main.rs │ └── server.rs ├── hotham-simulator ├── .vscode │ └── tasks.json ├── Cargo.toml ├── README.md ├── build.rs ├── build_input │ ├── loader_interfaces.h │ ├── openxr │ │ ├── openxr.h │ │ ├── openxr_platform.h │ │ ├── openxr_platform_defines.h │ │ ├── openxr_reflection.h │ │ └── xr_linear.c │ ├── viewdisplay.frag │ ├── viewdisplay.frag.spv │ ├── viewdisplay.vert │ ├── viewdisplay.vert.spv │ └── wrapper.h ├── hotham_simulator.json ├── openxr_simulator.reg ├── src │ ├── action_state.rs │ ├── bindings.rs │ ├── inputs.rs │ ├── lib.rs │ ├── openxr_loader.rs │ ├── shaders │ │ ├── viewdisplay.frag.spv │ │ └── viewdisplay.vert.spv │ ├── simulator.rs │ ├── space_state.rs │ └── state.rs ├── test.ps1 └── test.sh ├── hotham ├── Cargo.toml ├── build.rs ├── data │ ├── brdf_lut.ktx2 │ ├── brdf_lut_android.ktx2 │ ├── environment_map_diffuse.ktx2 │ ├── environment_map_diffuse_android.ktx2 │ ├── environment_map_specular.ktx2 │ └── environment_map_specular_android.ktx2 ├── matrix_failed.json └── src │ ├── asset_importer │ ├── mod.rs │ └── scene.rs │ ├── components │ ├── animation_controller.rs │ ├── animation_target.rs │ ├── global_transform.rs │ ├── grabbable.rs │ ├── hand.rs │ ├── hmd.rs │ ├── info.rs │ ├── joint.rs │ ├── local_transform.rs │ ├── mesh.rs │ ├── mod.rs │ ├── panel.rs │ ├── parent.rs │ ├── physics │ │ ├── additional_mass.rs │ │ ├── collider.rs │ │ ├── impulse.rs │ │ ├── mod.rs │ │ ├── rigid_body.rs │ │ └── teleport.rs │ ├── pointer.rs │ ├── root.rs │ ├── skin.rs │ ├── sound_emitter.rs │ ├── stage.rs │ ├── ui_panel.rs │ └── visible.rs │ ├── contexts │ ├── audio_context.rs │ ├── gui_context.rs │ ├── haptic_context.rs │ ├── input_context.rs │ ├── mod.rs │ ├── physics_context.rs │ ├── render_context.rs │ ├── vulkan_context.rs │ └── xr_context │ │ ├── input.rs │ │ ├── mod.rs │ │ └── time.rs │ ├── engine.rs │ ├── hotham_error.rs │ ├── lib.rs │ ├── rendering │ ├── buffer.rs │ ├── camera.rs │ ├── descriptors.rs │ ├── frame.rs │ ├── image.rs │ ├── legacy_buffer.rs │ ├── light.rs │ ├── material.rs │ ├── memory.rs │ ├── mesh_data.rs │ ├── mod.rs │ ├── primitive.rs │ ├── resources.rs │ ├── scene_data.rs │ ├── swapchain.rs │ ├── texture.rs │ └── vertex.rs │ ├── shaders │ ├── brdf.glsl │ ├── common.glsl │ ├── culling.comp │ ├── gui.frag │ ├── gui.vert │ ├── lights.glsl │ ├── pbr.frag │ ├── pbr.glsl │ └── pbr.vert │ ├── systems │ ├── animation.rs │ ├── audio.rs │ ├── debug.rs │ ├── draw_gui.rs │ ├── grabbing.rs │ ├── hands.rs │ ├── haptics.rs │ ├── mod.rs │ ├── physics.rs │ ├── pointers.rs │ ├── rendering.rs │ ├── skinning.rs │ └── update_global_transform.rs │ ├── util.rs │ └── workers │ └── mod.rs ├── logo.jpg ├── openxr_loader.dll ├── shell.nix └── test_assets ├── Quartet 14 - Clip.mp3 ├── asteroid.glb ├── box_no_material.glb ├── box_with_colliders.glb ├── cube.glb ├── culling_stress_test.glb ├── damaged_helmet.glb ├── damaged_helmet_squished.glb ├── ferris-the-crab ├── source │ └── ferris.glb └── textures │ └── ferris_eyes_0.png ├── floor.glb ├── gymnopedie.mp3 ├── hologram_templates.glb ├── ibl_test.glb ├── ibl_test_squished.glb ├── ice_crash.mp3 ├── left_hand.glb ├── left_hand_skinned_matrices.json ├── normal_tangent_mirror_test.glb ├── normal_tangent_test.glb ├── render_Diffuse.jpg ├── render_Diffuse_known_good.jpg ├── render_Full.jpg ├── render_Full_known_good.jpg ├── render_No_IBL.jpg ├── render_No_IBL_known_good.jpg ├── render_Normals.jpg ├── render_Normals_known_good.jpg ├── render_Spotlight.jpg ├── render_Spotlight_known_good.jpg ├── render_draw_gui.jpg ├── render_draw_gui_known_good.jpg ├── render_normal_tangent_test_IBL.jpg ├── render_normal_tangent_test_IBL_and_spotlight.jpg ├── render_normal_tangent_test_IBL_and_spotlight_known_good.jpg ├── render_normal_tangent_test_IBL_known_good.jpg ├── render_normal_tangent_test_Normals.jpg ├── render_normal_tangent_test_Normals_known_good.jpg ├── render_normal_tangent_test_Spotlight1.jpg ├── render_normal_tangent_test_Spotlight1_known_good.jpg ├── render_normal_tangent_test_Spotlight2.jpg ├── render_normal_tangent_test_Spotlight2_known_good.jpg ├── renderdoc_performance_counters.json ├── right_hand.glb ├── right_hand_skinned_matrices.json ├── right_here_beside_you_clip.mp3 ├── sphere.glb └── tell_me_that_i_cant_clip.mp3 /.gitattributes: -------------------------------------------------------------------------------- 1 | *.glb filter=lfs diff=lfs merge=lfs -text 2 | *.so filter=lfs diff=lfs merge=lfs -text 3 | *.ktx2 filter=lfs diff=lfs merge=lfs -text 4 | *.mp3 filter=lfs diff=lfs merge=lfs -text 5 | *.jpg filter=lfs diff=lfs merge=lfs -text 6 | *.png filter=lfs diff=lfs merge=lfs -text 7 | test_assets/Quartet[[:space:]]14[[:space:]]-[[:space:]]Clip.mp3 filter=lfs diff=lfs merge=lfs -text 8 | test_assets/culling_stress_test.glb filter=lfs diff=lfs merge=lfs -text 9 | hotham/data/brdf_lut.ktx2 filter=lfs diff=lfs merge=lfs -text 10 | hotham/data/environment_map_diffuse.ktx2 filter=lfs diff=lfs merge=lfs -text 11 | hotham/data/environment_map_specular.ktx2 filter=lfs diff=lfs merge=lfs -text 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [leetvr] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-task.yaml: -------------------------------------------------------------------------------- 1 | name: Hotham Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: Also tell us, what did you expect to happen? 15 | placeholder: Tell us what you saw! 16 | value: "A bug happened!" 17 | validations: 18 | required: true 19 | - type: dropdown 20 | id: version 21 | attributes: 22 | label: Version 23 | description: What version of our software are you running? 24 | options: 25 | - 0.1.1 (Default) 26 | - latest 27 | validations: 28 | required: true 29 | - type: dropdown 30 | id: hotham-component 31 | attributes: 32 | label: In which Hotham component are you seeing the problem on? 33 | multiple: true 34 | options: 35 | - Core 36 | - Example 37 | - Simulator 38 | - Debug 39 | - Other 40 | - type: dropdown 41 | id: vr-system 42 | attributes: 43 | label: What VR System are you seeing the problem on? 44 | multiple: false 45 | options: 46 | - Quest-2 47 | - Quest-1 48 | - Other 49 | - type: dropdown 50 | id: operating-system 51 | attributes: 52 | label: What OS are you seeing the problem on? 53 | multiple: true 54 | options: 55 | - Linux 56 | - MacOS 57 | - Windows 58 | - type: textarea 59 | id: logs 60 | attributes: 61 | label: Relevant log output 62 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 63 | render: shell 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/maintenance-task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Maintenance Task 3 | about: Something needs maintaining! 4 | labels: maintenance 5 | --- 6 | 7 | ## Description 8 | Description goes here! Include why this might be helpful 9 | 10 | ## TODO 11 | - [ ] 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull-request-task.md: -------------------------------------------------------------------------------- 1 | ## What is this related to? 2 | 3 | ## What changed? 4 | 5 | ## What is the overall impact? 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: [main] 4 | 5 | name: CI 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | RUST_TOOLCHAIN: stable 10 | TOOLCHAIN_PROFILE: minimal 11 | 12 | jobs: 13 | lints: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Install ALSA dev 18 | run: | 19 | sudo apt-get update 20 | sudo apt-get install libasound2-dev 21 | - name: Checkout sources 22 | uses: actions/checkout@v2 23 | - name: Install toolchain 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | profile: ${{ env.TOOLCHAIN_PROFILE }} 27 | toolchain: ${{ env.RUST_TOOLCHAIN }} 28 | target: aarch64-linux-android 29 | override: true 30 | components: rustfmt, clippy 31 | - name: Install Android NDK 32 | run: | 33 | sudo apt-get install curl unzip 34 | curl https://dl.google.com/android/repository/android-ndk-r22b-linux-x86_64.zip > /tmp/ndk.zip 35 | sudo unzip -d /ndk /tmp/ndk.zip 36 | echo /ndk/android-ndk-r22b/toolchains/llvm/prebuilt/linux-x86_64/bin >> $GITHUB_PATH 37 | - name: Cache 38 | uses: Swatinem/rust-cache@v1 39 | - name: Run cargo fmt 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: fmt 43 | args: --all -- --check 44 | - name: Run cargo clippy 45 | uses: actions-rs/cargo@v1 46 | with: 47 | command: clippy 48 | args: --tests -- -D warnings 49 | - name: Run cargo clippy (android) 50 | uses: actions-rs/cargo@v1 51 | with: 52 | command: check 53 | args: --target aarch64-linux-android 54 | 55 | spellcheck: 56 | name: CSpell 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v3 60 | - uses: streetsidesoftware/cspell-action@v2 61 | with: 62 | files: | 63 | "**/*.md" 64 | 65 | test: 66 | name: Test 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Install ALSA dev 70 | run: | 71 | sudo apt-get update 72 | sudo apt-get install libasound2-dev 73 | - name: Checkout sources 74 | uses: actions/checkout@v2 75 | - name: Install toolchain 76 | uses: actions-rs/toolchain@v1 77 | with: 78 | profile: ${{ env.TOOLCHAIN_PROFILE }} 79 | toolchain: ${{ env.RUST_TOOLCHAIN }} 80 | override: true 81 | - name: Cache 82 | uses: Swatinem/rust-cache@v1 83 | - name: Run cargo test --no-run 84 | uses: actions-rs/cargo@v1 85 | with: 86 | command: test 87 | args: '--no-run' 88 | - name: Run cargo test 89 | uses: actions-rs/cargo@v1 90 | env: 91 | RUST_TEST_THREADS: 1 92 | with: 93 | command: test 94 | args: '-p hotham --lib' 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */target 2 | Cargo.lock 3 | target 4 | 5 | # Ignore test assets 6 | sponza* 7 | 8 | # Note that we NEVER care about uncompressed glTF stuff 9 | *.gltf 10 | *.bin 11 | *.jpg 12 | *.png 13 | *.ktx2 14 | *.ktx 15 | 16 | # Ignore logs 17 | *.log 18 | 19 | # Don't ignore images from rendering tests 20 | !render_*.jpg 21 | 22 | # Ignore compiled shaders 23 | *.spv 24 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/.rustfmt.toml -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "raczzalan.webgl-glsl-editor" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "(Windows) Launch", 9 | "type": "cppvsdbg", 10 | "request": "launch", 11 | "program": "enter program name, for example ${workspaceFolder}/a.exe", 12 | "args": [], 13 | "stopAtEntry": false, 14 | "cwd": "${fileDirname}", 15 | "environment": [], 16 | "console": "externalTerminal" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", 3 | "[glsl]": { 4 | "editor.formatOnSave": false 5 | }, 6 | "files.insertFinalNewline": true, 7 | "files.trimFinalNewlines": true, 8 | "files.trimTrailingWhitespace": true, 9 | // Uncomment this to tell rust-analyzer to treat the code as being compiled for android. 10 | // "rust-analyzer.cargo.target": "aarch64-linux-android", 11 | "webgl-glsl-editor.format.placeSpaceAfterKeywords": true 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "cargo", 6 | "command": "test", 7 | "args": [ 8 | "-p", 9 | "hotham", 10 | "--", 11 | "--test-threads", 12 | "1", 13 | "--nocapture" 14 | ], 15 | "env": { 16 | "RUST_BACKTRACE": "1" 17 | }, 18 | "problemMatcher": [ 19 | "$rustc" 20 | ], 21 | "label": "Run Hotham test suite" 22 | }, 23 | { 24 | "type": "cargo", 25 | "command": "test", 26 | "args": [ 27 | "-p", 28 | "hotham", 29 | "--", 30 | "systems::grabbing::tests::test_grabbing_system", 31 | "--nocapture" 32 | ], 33 | "env": { 34 | "RUST_BACKTRACE": "1" 35 | }, 36 | "problemMatcher": [ 37 | "$rustc" 38 | ], 39 | "label": "Run a specific test", 40 | "group": { 41 | "kind": "test", 42 | "isDefault": false 43 | } 44 | }, 45 | { 46 | "type": "cargo", 47 | "command": "run", 48 | "args": [ 49 | "--bin", 50 | "hotham_stress_test_desktop", 51 | "--release" 52 | ], 53 | "env": { 54 | "RUST_BACKTRACE": "1" 55 | }, 56 | "problemMatcher": [ 57 | "$rustc" 58 | ], 59 | "label": "Run Hotham stress test on the simulator", 60 | "group": { 61 | "kind": "test", 62 | "isDefault": false 63 | } 64 | }, 65 | { 66 | "type": "cargo", 67 | "command": "run", 68 | "args": [ 69 | "--bin", 70 | "hotham_simple_scene_example", 71 | "--release" 72 | ], 73 | "env": { 74 | "RUST_BACKTRACE": "1" 75 | }, 76 | "problemMatcher": [ 77 | "$rustc" 78 | ], 79 | "label": "Run Hotham simple scene on the simulator", 80 | "group": { 81 | "kind": "test", 82 | "isDefault": false, 83 | } 84 | }, 85 | { 86 | "type": "cargo", 87 | "command": "run", 88 | "args": [ 89 | "--bin", 90 | "hotham_asset_server", 91 | ], 92 | "env": { 93 | "RUST_BACKTRACE": "1" 94 | }, 95 | "problemMatcher": [ 96 | "$rustc" 97 | ], 98 | "label": "Run Hotham simple scene on the simulator", 99 | "group": { 100 | "kind": "test", 101 | "isDefault": true, 102 | } 103 | }, 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## UNRELEASED 8 | ### Changed 9 | - Fixed default hand glTF files so offsets are not required when applied to grip pose - @rasmusgo [#271](https://github.com/leetvr/hotham/pull/271) 10 | 11 | ## [0.2] - 2022-05-10 12 | ### Added 13 | - Developers can now add their own application name, version and OpenXR layers to the `Engine` - @jmgao [#197](https://github.com/leetvr/hotham/pull/197) 14 | 15 | ### Changed 16 | - `Panel`s are now independent from `egui`, thanks to @jmgao's fantastic work. `Panel` can now have variable sizes and developers are able to add their own custom content. Existing functionality has been moved to `UiPanel`. - @jmgao [#188](https://github.com/leetvr/hotham/issues/188). 17 | 18 | ### Fixed 19 | - Hotham and Hotham Simulator now no longer have hard dependencies on Vulkan Validation layers. [#182](https://github.com/leetvr/hotham/issues/182) 20 | - Hotham Simulator no longer segfaults on close - @jmgao [#185](https://github.com/leetvr/hotham/issues/185) 21 | - Android debug build is no longer broken - @jmgao [#186](https://github.com/leetvr/hotham/issues/186) 22 | - Fixed ANR on volume up / down - @jmgao [#190](https://github.com/leetvr/hotham/issues/190) 23 | - Bump to `ndk-glue` - @jmgao [#201](https://github.com/leetvr/hotham/issues/201) 24 | 25 | ### Maintenance 26 | - Significant CI improvements, including `clippy` - @davidkuhta [#160](https://github.com/leetvr/hotham/issues/160) 27 | 28 | 29 | ## 0.1 - 2022-03-01 30 | Initial release! 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to Hotham 2 | First of all, thank you so much for your interest and support in Hotham. As a small, open source project, our success is only possible because of people like **you**. 3 | 4 | ## Filing bugs 🐛 5 | Bug reports are some of the most helpful things you can add. Did the example project fail to run on your headset? Did the engine crash when trying to run your game? Did the model importer panic when importing a model? Please, file and issue and **let us know**! 6 | 7 | ## General questions 💬 8 | The best place to get a quick question answered is in [the Hotham discord](https://discord.gg/SZEZUX6ZsQ). There you can speak with the Hotham core team, as well as other developers who are also working on their own projects with Hotham. 9 | 10 | ## Contributing code 🔨 11 | Fixed a bug? Got a suggestion for a great feature? Open a pull request! While we strive to keep the scope of the project small (making VR applications for mobile headsets), we're also open to new ideas. if you're thinking of contributing a large feature, it might be a good idea to get in touch first to see if that would be a good fit for the future road-map, or whether it's something that's planned for later on. 12 | 13 | ## Sponsoring 💰 14 | If you're light on time, why not [contribute some of your hard earned cash money for the project](https://github.com/sponsors/leetvr)? Your funds help enable more developers to work full time on the project. 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "benchmarks/stress-test", 4 | "examples/complex-scene", 5 | "examples/crab-saber", 6 | "examples/custom-rendering", 7 | "examples/shared", 8 | "examples/simple-scene", 9 | "hotham-asset-client", 10 | "hotham-asset-server", 11 | "hotham-simulator", 12 | "hotham", 13 | ] 14 | 15 | # Make performance critical packages compile with optimizations 16 | [profile.dev.package.rapier3d] 17 | opt-level = 3 18 | 19 | [profile.dev.package.parry3d] 20 | opt-level = 3 21 | 22 | [profile.dev.package.nalgebra] 23 | opt-level = 3 24 | 25 | [profile.dev.package.glam] 26 | opt-level = 3 27 | 28 | [profile.dev.package.gltf] 29 | opt-level = 3 30 | 31 | [profile.dev.package.gltf-json] 32 | opt-level = 3 33 | 34 | [profile.dev.package.serde] 35 | opt-level = 3 36 | 37 | [profile.dev.package.image] 38 | opt-level = 3 39 | 40 | [profile.dev.package.png] 41 | opt-level = 3 42 | 43 | [profile.dev.package.bytemuck] 44 | opt-level = 3 45 | 46 | [profile.dev.package.symphonia] 47 | opt-level = 3 48 | 49 | [profile.dev.package.symphonia-core] 50 | opt-level = 3 51 | 52 | [profile.dev.package.symphonia-bundle-mp3] 53 | opt-level = 3 54 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Let Eyes Equals Two Pty Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/leetvr/hotham/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/leetvr/hotham/actions/workflows/ci.yaml) 2 | [![Documentation](https://docs.rs/hotham/badge.svg)](https://docs.rs/hotham/) 3 | [![Crates.io](https://img.shields.io/crates/v/hotham.svg)](https://crates.io/crates/hotham) 4 | [![Discord](https://img.shields.io/discord/930970630612979782?label=Hotham%20Discord)](https://discord.gg/SZEZUX6ZsQ) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT) 6 | [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE-APACHE) 7 | 8 | ![Hotham Logo](logo.jpg?raw=true) 9 | 10 | # Introduction 11 | G'day, and welcome to Hotham! 👋 12 | 13 | Hotham is an attempt to create a lightweight, high performance game engine for standalone VR headsets, like the Oculus Quest 2. 14 | 15 | It's primarily aimed at small (1-5 person) teams of mostly technical folk who are looking to create VR games, but find existing tools cumbersome to work with. You can learn more about the project [in the FAQ](https://github.com/leetvr/hotham/wiki/FAQ) or by checking out [our talk at the Rust Gamedev Meetup](https://youtu.be/adt63Gqt6yA?t=717). 16 | 17 | # Getting started 18 | Hotham is a complex project with many moving parts! Have no fear - we've written an easy to follow [Getting Started guide](https://github.com/leetvr/hotham/wiki/Getting-started) that will have you running our example application in no time. Head on over to [getting started](https://github.com/leetvr/hotham/wiki/Getting-started) to.. get.. started. 19 | 20 | # Sponsoring 21 | Hotham's development is only possible thanks to the support of the community. It's currently being developed on full time by [@kanerogers](https://github.com/kanerogers) If you'd like to help make VR development in Rust possible, please [consider becoming a donor](https://github.com/sponsors/leetvr). 💗 22 | 23 | ## Featured sponsors 24 | These generous people contribute $50 or more each month to support Hotham ❤️ 25 | 26 |

27 | Arto Bendiken 28 |

29 | 30 | # Progress 🔨 31 | - [x] Vulkan renderer 32 | - [x] OpenXR integration 33 | - [x] Android (eg. Oculus Quest) support 34 | - [x] Simple Windows-based OpenXR simulator 35 | - [x] Import of [glTF](https://www.khronos.org/gltf/) models 36 | - [x] Support for skinned models 37 | - [x] Support for animations 38 | - [x] Hand/controller presence 39 | - [x] Object grabbing support 40 | - [x] Physics (eg. collision detection) support 41 | - [x] [Basic PBR support](https://github.com/leetvr/hotham/issues/65) 42 | - [x] [Simple debug UI](https://github.com/leetvr/hotham/issues/96) 43 | - [x] [Sound support](https://github.com/leetvr/hotham/issues/52) 44 | - [x] [Text/UI support](https://github.com/leetvr/hotham/issues/93) 45 | 46 | # Current Demo 47 | ![Crab Saber](https://user-images.githubusercontent.com/2022375/152466675-b6a28557-4cbb-4acc-8177-f2126179ef02.gif) 48 | 49 | # Future goals ✨ 50 | - [ ] Visual editor 51 | - [ ] Fast reloading 52 | - [ ] OpenXR record/playback for integration testing 53 | - [ ] Better debugging support 54 | - [ ] Better performance profiling support 55 | 56 | ## License 57 | 58 | Licensed under either of 59 | 60 | * Apache License, Version 2.0 61 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 62 | * MIT license 63 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 64 | 65 | at your option. 66 | 67 | ## Contribution 68 | 69 | Unless you explicitly state otherwise, any contribution intentionally submitted 70 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 71 | dual licensed as above, without any additional terms or conditions. 72 | 73 | ## Disclaimer 74 | 1. Trademarks are the property of their respective owners 75 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | ## Many objects 4 | ### Methodology 5 | - Each second, add a cube (in this case, the default Blender cube, exported to glTF) 6 | - At the moment of crash, record the number of cubes and current FPS 7 | 8 | ### Hotham 0.2 9 | - **Result**: Crash - `Result::unwrap()` on an `Err` value: ERROR_OUT_OF_POOL_MEMORY', hotham\src\gltf_loader.rs:448:14` 10 | - **Max cubes**: 98 11 | - **FPS at time of crash**: 72 12 | - **OVR stats**: `FPS=72/72,Prd=28ms,Tear=0,Early=0,Stale=1,VSnc=0,Lat=-1,Fov=0,CPU4/GPU=2/3,1171/490MHz,OC=FF,TA=0/0/0,SP=N/N/N,Mem=1353MHz,Free=3274MB,PLS=0,Temp=28.0C/0.0C,TW=2.39ms,App=1.73ms,GD=0.27ms,CPU&GPU=4.08ms,LCnt=1,GPU%=0.35,CPU%=0.12(W0.14),DSF=1.00` 13 | 14 | ## Many vertices 15 | ### Methodology 16 | - Start with a mesh with two triangles. 17 | - Each second, increase the number of vertices 18 | - Record FPS vs Vertices 19 | - At the moment of crash, record the number of vertices and last FPS 20 | 21 | ### Hotham 0.2 22 | - **Result**: Crash - segfault 23 | - **Max vertices**: 1,040,400 vertices, 24 | - **FPS at time of crash**: 16 25 | - **OVR stats at time of crash**: `FPS=16/72,Prd=83ms,Tear=2,Early=2,Stale=56,VSnc=0,Lat=-1,Fov=0,CPU4/GPU=4/3,1478/490MHz,OC=FF,TA=0/0/0,SP=N/N/N,Mem=1353MHz,Free=3200MB,PLS=0,Temp=31.0C/0.0C,TW=8.91ms,App=11.80ms,GD=0.00ms,CPU&GPU=58.12ms,LCnt=1,GPU%=1.00,CPU%=0.02(W0.05),DSF=1.00` 26 | - **Highest vertex count at 72 FPS**: 144,400 27 | 28 | ## Sponza 29 | ### Methodology 30 | - Download the [new Sponza scene](https://www.intel.com/content/www/us/en/developer/topic-technology/graphics-research/samples.html) 31 | - Export the glTF file into a GLB file with Blender 32 | - Place the glTF file in the `test_assets` directory. 33 | - Record the FPS if the scene loads 34 | 35 | ### Hotham 0.2 36 | - **Result**: Crash - `ERROR_OUT_OF_POOL_MEMORY` 37 | - **Note**: Was unable to run in simulator so did not attempt to run in headset. Will require further investigation to load the GLB from the device's internal storage as it is too large to either include in the binary or the APK. -------------------------------------------------------------------------------- /benchmarks/stress-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | license = "MIT OR Apache-2.0" 4 | name = "hotham-stress-test" 5 | version = "0.2.0" 6 | 7 | [lib] 8 | crate-type = ["lib", "cdylib"] 9 | 10 | [[bin]] 11 | name = "hotham_stress_test_desktop" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | hotham = {path = "../../hotham"} 16 | 17 | [target.'cfg(target_os = "android")'.dependencies] 18 | ndk-glue = "0.6" 19 | 20 | [package.metadata.android] 21 | apk_label = "Hotham Stress Test" 22 | fullscreen = true 23 | runtime_libs = "../../examples/common_lib" 24 | target_sdk_version = 29 25 | 26 | [package.metadata.android.application] 27 | debuggable = true 28 | label = "Hotham Stress Test" 29 | theme = "@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen" 30 | 31 | [package.metadata.android.application.activity] 32 | config_changes = "screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode" 33 | launch_mode = "singleTask" 34 | orientation = "landscape" 35 | 36 | [[package.metadata.android.uses_permission]] 37 | name = "android.permission.INTERNET" 38 | 39 | [[package.metadata.android.uses_permission]] 40 | name = "android.permission.ACCESS_NETWORK_STATE" 41 | 42 | [[package.metadata.android.application.meta_data]] 43 | name = "com.oculus.supportedDevices" 44 | value = "quest|quest2" 45 | 46 | [[package.metadata.android.application.meta_data]] 47 | name = "com.oculus.intent.category.VR" 48 | value = "vr_only" 49 | 50 | [[package.metadata.android.application.activity.intent_filter]] 51 | actions = ["android.intent.action.MAIN"] 52 | categories = ["com.oculus.intent.category.VR", "android.intent.category.LAUNCHER"] 53 | 54 | [[package.metadata.android.application.activity.meta_data]] 55 | name = "com.oculus.vr.focusaware" 56 | value = "true" 57 | 58 | [[package.metadata.android.uses_feature]] 59 | name = "android.hardware.vulkan.level" 60 | required = true 61 | version = 1 62 | 63 | [[package.metadata.android.uses_feature]] 64 | name = "android.hardware.vr.headtracking" 65 | required = true 66 | version = 1 67 | 68 | # !! IMPORTANT !! 69 | # 70 | # When creating your own apps, make sure to generate your own keystore, rather than using our example one! 71 | # You can use `keytool` like so: 72 | # keytool -genkey -v -keystore my-release-key.keystore -keyalg RSA -keysize 2048 -validity 10000 73 | # 74 | # For more information on key signing and why it's so important, check out this article: 75 | # https://developer.android.com/studio/publish/app-signing 76 | # 77 | # !! IMPORTANT !! 78 | [package.metadata.android.signing.release] 79 | path = "../../examples/hotham_examples.keystore" 80 | keystore_password = "chomsky-vigilant-spa" 81 | -------------------------------------------------------------------------------- /benchmarks/stress-test/README.md: -------------------------------------------------------------------------------- 1 | ![Stress Test](logo.png?raw=true) 2 | # Stress Test 3 | This project contains a series of tests designed to *torture* Hotham. It is unfair, unkind and it most certainly does not care what you think of it. 4 | 5 | ## How to run the tests 6 | Switching between the various tests is accomplished by changing the `test` variable in `src/lib.rs` to some variant of the `StressTest` enum: 7 | 8 | ```rust 9 | /// Used to select which test to run 10 | pub enum StressTest { 11 | /// Generate one cube per second 12 | ManyCubes, 13 | /// Create a dynamic mesh and increase the number of vertices each second 14 | ManyVertices, 15 | /// Load the New Sponza scene into the engine 16 | Sponza, 17 | } 18 | ``` 19 | 20 | For more information on these tests, and their results, consult the `README.md` file in the parent directory. 21 | -------------------------------------------------------------------------------- /benchmarks/stress-test/logo.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4817a1d3abf7c0ca25fb8cd0b93deeec532fe67303c5aba09f507f62f0373521 3 | size 124631 4 | -------------------------------------------------------------------------------- /benchmarks/stress-test/scripts/run_on_device.ps1: -------------------------------------------------------------------------------- 1 | ${env:RUST_BACKTRACE} = 1 2 | adb shell am force-stop rust.hotham_stress_test 3 | 4 | Set-Location $PSScriptRoot\.. 5 | cargo apk run --release 6 | 7 | if ($?) { 8 | $processId = $null 9 | foreach ($i in 1..5) { 10 | $processId = adb shell pidof rust.hotham_stress_test 11 | if ($processId) { break } 12 | Write-Output "Waiting for process to start, sleeping..." 13 | Start-Sleep -Seconds 1 14 | } 15 | if ($processId) { 16 | Write-Output "Found PID of " $processId 17 | adb logcat --pid=$processId 18 | } else { 19 | Write-Error "Failed to find PID of rust.hotham_stress_test" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /benchmarks/stress-test/scripts/run_on_device.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | adb shell am force-stop rust.hotham_stress_test 5 | 6 | scriptdir=$(dirname -- "$(realpath -- "$0")") 7 | cd $scriptdir/.. 8 | 9 | cargo apk run --release 10 | 11 | # Wait for the app to start 12 | for i in 1 2 3 4 5; do 13 | adb shell pidof rust.hotham_stress_test && break 14 | sleep 1 15 | done 16 | 17 | adb logcat --pid="$(adb shell pidof rust.hotham_stress_test)" 18 | -------------------------------------------------------------------------------- /benchmarks/stress-test/src/main.rs: -------------------------------------------------------------------------------- 1 | use hotham::HothamResult; 2 | 3 | fn main() -> HothamResult<()> { 4 | hotham_stress_test::real_main() 5 | } 6 | -------------------------------------------------------------------------------- /benchmarks/stress-test/src/systems/cube.rs: -------------------------------------------------------------------------------- 1 | use hotham::{ 2 | asset_importer::{add_model_to_world, Models}, 3 | components::LocalTransform, 4 | glam::Vec3, 5 | hecs::World, 6 | }; 7 | 8 | pub fn setup_cubes(world: &mut World, resolution: usize, models: &Models) { 9 | let step = 2. / resolution as f32; 10 | let scale_factor = 3.; 11 | let scale = Vec3::splat(step / scale_factor); 12 | let half_resolution = resolution as f32 / 2.; 13 | let x_offset = half_resolution / scale_factor; 14 | 15 | for floor in 0..resolution { 16 | for row in 0..resolution { 17 | for column in 0..resolution { 18 | let c = add_model_to_world("Cube", models, world, None).unwrap(); 19 | let mut t = world.get::<&mut LocalTransform>(c).unwrap(); 20 | t.scale = scale; 21 | t.translation.y = floor as f32 / scale_factor; 22 | t.translation.x = column as f32 / scale_factor - x_offset; 23 | t.translation.z = row as f32 / scale_factor - half_resolution - 2.0; 24 | } 25 | } 26 | } 27 | } 28 | 29 | // Different ways of arranging items 30 | // fn circle(u: f32, v: f32, _t: f32) -> Vector3 { 31 | // let x = (PI * u).sin(); 32 | // let y = v; 33 | // let z = (PI * u).cos(); 34 | // [x, y, z].into() 35 | // } 36 | 37 | // fn sphere(u: f32, v: f32, _t: f32) -> Vector3 { 38 | // let r = 1.; 39 | // let s = r * (0.5 * PI * v).cos(); 40 | 41 | // let x = s * (PI * u).sin(); 42 | // let y = r * (PI * 0.5 * v).sin(); 43 | // let z = s * (PI * u).cos(); 44 | // [x, y, z].into() 45 | // } 46 | 47 | // fn _torus(u: f32, v: f32, _t: f32) -> Vector3 { 48 | // let r1 = 0.75; 49 | // let r2 = 0.25; 50 | // let s = r1 + r2 * (PI * v).cos(); 51 | 52 | // let x = s * (PI * u).sin(); 53 | // let y = r2 * (PI * v).sin(); 54 | // let z = s * (PI * u).cos(); 55 | // [x, y, z].into() 56 | // } 57 | -------------------------------------------------------------------------------- /benchmarks/stress-test/src/systems/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cube; 2 | pub use cube::setup_cubes; 3 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | // cSpell Settings 2 | { 3 | // Version of the setting file. Always 0.2 4 | "version": "0.2", 5 | // language - current active spelling language 6 | "language": "en", 7 | // words - list of words to be always considered correct 8 | "words": [ 9 | "aarch", 10 | "ALSA", 11 | "bindgen", 12 | "brdf", 13 | "brdflut", 14 | "Brickner", 15 | "Carmack", 16 | "cdylib", 17 | "clippy", 18 | "consts", 19 | "davidkuhta", 20 | "debuggable", 21 | "DONT", 22 | "dylib", 23 | "egui", 24 | "emath", 25 | "epaint", 26 | "Fovf", 27 | "framebuffer", 28 | "framebuffers", 29 | "fullscreen", 30 | "Gamedev", 31 | "glsl", 32 | "gltf", 33 | "grabbable", 34 | "haptics", 35 | "Hasher", 36 | "hecs", 37 | "Hemsley", 38 | "hotham", 39 | "intrinsics", 40 | "itertools", 41 | "izip", 42 | "jmgao", 43 | "kanerogers", 44 | "KHRONOS", 45 | "Lambertian", 46 | "lerp", 47 | "Lhand", 48 | "libasound", 49 | "libktx", 50 | "libovr", 51 | "linvel", 52 | "Luckey", 53 | "memoffset", 54 | "metalness", 55 | "microfacet", 56 | "mipmap", 57 | "mipmaps", 58 | "MSAA", 59 | "multibody", 60 | "multisample", 61 | "multisampling", 62 | "multiview", 63 | "nalgebra", 64 | "nanos", 65 | "NEFFEX", 66 | "NONBLOCKING", 67 | "nonoverlapping", 68 | "oddio", 69 | "openxr", 70 | "openxrs", 71 | "Photometria", 72 | "prefiltered", 73 | "Quaternionf", 74 | "Ralith", 75 | "rasmusgo", 76 | "rasterization", 77 | "rasterizer", 78 | "renderdoc", 79 | "renderpass", 80 | "renderpasses", 81 | "repr", 82 | "rezural", 83 | "rgba", 84 | "rustc", 85 | "rustfmt", 86 | "Sascha", 87 | "serde", 88 | "shaderc", 89 | "SIGGRAPH", 90 | "sinoth", 91 | "slerp", 92 | "socio", 93 | "spatialized", 94 | "spatializer", 95 | "spirv", 96 | "Sponza", 97 | "Squisher", 98 | "srgb", 99 | "struct", 100 | "subaction", 101 | "subpass", 102 | "subpasses", 103 | "subresource", 104 | "supercompression", 105 | "swapchain", 106 | "swapchains", 107 | "Swatinem", 108 | "thiserror", 109 | "thumbstick", 110 | "tonemap", 111 | "toolchains", 112 | "tungstenite", 113 | "Uninit", 114 | "unmap", 115 | "unnormalized", 116 | "viewports", 117 | "vulkan", 118 | "wbrickner", 119 | "Willems", 120 | "winit" 121 | ], 122 | "ignoreWords": [ 123 | "Adreno", 124 | "basisu", 125 | "bvec3", 126 | "Ldot", 127 | "Ndot", 128 | "outcol", 129 | "Posef", 130 | "Reitz", 131 | "SFLOAT", 132 | "UNORM", 133 | "Vdot", 134 | "viewdisplay", 135 | "writeonly", 136 | "xyzw" 137 | ], 138 | "flagWords": [ 139 | "centre" 140 | ], 141 | "ignorePaths": [ 142 | "**/*.bin", 143 | "**/*.glb", 144 | "**/*.ktx2", 145 | "**/*.mp3", 146 | "**/*.so", 147 | "**/node_modules/**", 148 | "**/target/**", 149 | "cspell.json" 150 | ], 151 | "enableFiletypes": [ 152 | "glsl" 153 | ] 154 | } 155 | -------------------------------------------------------------------------------- /docs/hotham_sync_loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/docs/hotham_sync_loop.png -------------------------------------------------------------------------------- /examples/common_lib/arm64-v8a/libopenxr_loader.so: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:36ffaa34b35733229a56fd47d434b0b3ceb423e71857918799c29018c8209c36 3 | size 8706760 4 | -------------------------------------------------------------------------------- /examples/complex-scene/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | license = "MIT OR Apache-2.0" 4 | name = "complex-scene-example" 5 | version = "0.2.0" 6 | 7 | [lib] 8 | crate-type = ["lib", "cdylib"] 9 | 10 | [[bin]] 11 | name = "hotham_complex_scene_example" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | hotham = {path = "../../hotham"} 16 | hotham-examples = {path = "../shared"} 17 | 18 | [target.'cfg(target_os = "android")'.dependencies] 19 | ndk-glue = "0.6" 20 | 21 | [package.metadata.android] 22 | apk_label = "Hotham Complex Scene Example" 23 | fullscreen = true 24 | runtime_libs = "../common_lib" 25 | target_sdk_version = 29 26 | 27 | [package.metadata.android.application] 28 | debuggable = true 29 | label = "Hotham Complex Scene Example" 30 | theme = "@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen" 31 | 32 | [package.metadata.android.application.activity] 33 | config_changes = "screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode" 34 | launch_mode = "singleTask" 35 | orientation = "landscape" 36 | 37 | [[package.metadata.android.uses_permission]] 38 | name = "android.permission.INTERNET" 39 | 40 | [[package.metadata.android.uses_permission]] 41 | name = "android.permission.ACCESS_NETWORK_STATE" 42 | 43 | [[package.metadata.android.application.meta_data]] 44 | name = "com.oculus.supportedDevices" 45 | value = "quest|quest2" 46 | 47 | [[package.metadata.android.application.meta_data]] 48 | name = "com.oculus.intent.category.VR" 49 | value = "vr_only" 50 | 51 | [[package.metadata.android.application.activity.intent_filter]] 52 | actions = ["android.intent.action.MAIN"] 53 | categories = ["com.oculus.intent.category.VR", "android.intent.category.LAUNCHER"] 54 | 55 | [[package.metadata.android.application.activity.meta_data]] 56 | name = "com.oculus.vr.focusaware" 57 | value = "true" 58 | 59 | [[package.metadata.android.uses_feature]] 60 | name = "android.hardware.vulkan.level" 61 | required = true 62 | version = 1 63 | 64 | [[package.metadata.android.uses_feature]] 65 | name = "android.hardware.vr.headtracking" 66 | required = true 67 | version = 1 68 | 69 | # !! IMPORTANT !! 70 | # 71 | # When creating your own apps, make sure to generate your own keystore, rather than using our example one! 72 | # You can use `keytool` like so: 73 | # keytool -genkey -v -keystore my-release-key.keystore -keyalg RSA -keysize 2048 -validity 10000 74 | # 75 | # For more information on key signing and why it's so important, check out this article: 76 | # https://developer.android.com/studio/publish/app-signing 77 | # 78 | # !! IMPORTANT !! 79 | [package.metadata.android.signing.release] 80 | path = "../hotham_examples.keystore" 81 | keystore_password = "chomsky-vigilant-spa" 82 | -------------------------------------------------------------------------------- /examples/complex-scene/README.md: -------------------------------------------------------------------------------- 1 | # Complex Scene 2 | This scene is using some of Hotham's more complex features. 3 | -------------------------------------------------------------------------------- /examples/complex-scene/openxr_loader.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/examples/complex-scene/openxr_loader.dll -------------------------------------------------------------------------------- /examples/complex-scene/scripts/run_on_device.ps1: -------------------------------------------------------------------------------- 1 | adb shell am force-stop rust.complex_scene_example 2 | 3 | Set-Location $PSScriptRoot\.. 4 | cargo apk run --release 5 | 6 | if ($?) { 7 | Start-Sleep -Seconds 2 8 | $processIdStr = (adb shell pidof rust.complex_scene_example) | Out-String 9 | Write-Output $processIdStr 10 | $processId = $processIdStr -as [int] 11 | Write-Output $processId 12 | adb logcat --pid=$processId 13 | } 14 | -------------------------------------------------------------------------------- /examples/complex-scene/scripts/run_on_device.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | adb shell am force-stop rust.complex_scene_example 5 | 6 | scriptdir=$(dirname -- "$(realpath -- "$0")") 7 | cd $scriptdir/.. 8 | 9 | cargo apk run --release 10 | 11 | adb logcat --pid="$(adb shell pidof rust.complex_scene_example)" 12 | -------------------------------------------------------------------------------- /examples/complex-scene/src/lib.rs: -------------------------------------------------------------------------------- 1 | use hotham::{ 2 | asset_importer::{self, add_model_to_world}, 3 | components::{ 4 | hand::Handedness, 5 | physics::{BodyType, SharedShape}, 6 | Collider, Grabbable, LocalTransform, RigidBody, 7 | }, 8 | glam, 9 | hecs::World, 10 | na, 11 | systems::{ 12 | animation_system, debug::debug_system, grabbing_system, hands::add_hand, hands_system, 13 | physics_system, rendering::rendering_system, skinning::skinning_system, 14 | update_global_transform_system, 15 | }, 16 | xr, Engine, HothamResult, TickData, 17 | }; 18 | use hotham_examples::navigation::{navigation_system, State}; 19 | 20 | #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] 21 | pub fn main() { 22 | println!("[HOTHAM_COMPLEX_SCENE] MAIN!"); 23 | real_main().expect("Error running app!"); 24 | println!("[HOTHAM_COMPLEX_SCENE] FINISHED! Goodbye!"); 25 | } 26 | 27 | pub fn real_main() -> HothamResult<()> { 28 | let mut engine = Engine::new(); 29 | let mut state = Default::default(); 30 | init(&mut engine)?; 31 | 32 | while let Ok(tick_data) = engine.update() { 33 | tick(tick_data, &mut engine, &mut state); 34 | engine.finish()?; 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | fn tick(tick_data: TickData, engine: &mut Engine, state: &mut State) { 41 | if tick_data.current_state == xr::SessionState::FOCUSED { 42 | hands_system(engine); 43 | grabbing_system(engine); 44 | physics_system(engine); 45 | animation_system(engine); 46 | navigation_system(engine, state); 47 | update_global_transform_system(engine); 48 | skinning_system(engine); 49 | debug_system(engine); 50 | } 51 | 52 | rendering_system(engine, tick_data.swapchain_image_index); 53 | } 54 | 55 | fn init(engine: &mut Engine) -> Result<(), hotham::HothamError> { 56 | if option_env!("HOTHAM_ASSET_SERVER_ADDRESS").is_some() { 57 | #[cfg(target_os = "android")] 58 | engine.watch_assets(vec!["test_assets/damaged_helmet_squished.glb".into()]); 59 | #[cfg(not(target_os = "android"))] 60 | engine.watch_assets(vec!["test_assets/damaged_helmet.glb".into()]); 61 | } 62 | let render_context = &mut engine.render_context; 63 | let vulkan_context = &mut engine.vulkan_context; 64 | let world = &mut engine.world; 65 | 66 | let mut glb_buffers: Vec<&[u8]> = vec![ 67 | include_bytes!("../../../test_assets/floor.glb"), 68 | include_bytes!("../../../test_assets/left_hand.glb"), 69 | include_bytes!("../../../test_assets/right_hand.glb"), 70 | ]; 71 | 72 | #[cfg(target_os = "android")] 73 | glb_buffers.push(include_bytes!( 74 | "../../../test_assets/damaged_helmet_squished.glb" 75 | )); 76 | 77 | #[cfg(not(target_os = "android"))] 78 | glb_buffers.push(include_bytes!("../../../test_assets/damaged_helmet.glb")); 79 | 80 | let models = 81 | asset_importer::load_models_from_glb(&glb_buffers, vulkan_context, render_context)?; 82 | add_helmet(&models, world, [-1., 1.4, -1.].into()); 83 | add_helmet(&models, world, [1., 1.4, -1.].into()); 84 | add_hand(&models, Handedness::Left, world); 85 | add_hand(&models, Handedness::Right, world); 86 | add_floor(&models, world); 87 | 88 | Ok(()) 89 | } 90 | 91 | fn add_floor(models: &std::collections::HashMap, world: &mut World) { 92 | let entity = add_model_to_world("Floor", models, world, None).expect("Could not find Floor"); 93 | let collider = Collider::new(SharedShape::halfspace(na::Vector3::y_axis())); 94 | let rigid_body = RigidBody { 95 | body_type: BodyType::Fixed, 96 | ..Default::default() 97 | }; 98 | world.insert(entity, (collider, rigid_body)).unwrap(); 99 | } 100 | 101 | fn add_helmet( 102 | models: &std::collections::HashMap, 103 | world: &mut World, 104 | translation: glam::Vec3, 105 | ) { 106 | let helmet = add_model_to_world("Damaged Helmet", models, world, None) 107 | .expect("Could not find Damaged Helmet"); 108 | 109 | { 110 | let mut local_transform = world.get::<&mut LocalTransform>(helmet).unwrap(); 111 | local_transform.translation = translation; 112 | local_transform.scale = [0.5, 0.5, 0.5].into(); 113 | } 114 | 115 | let collider = Collider::new(SharedShape::ball(0.35)); 116 | 117 | world.insert(helmet, (collider, Grabbable {})).unwrap(); 118 | } 119 | -------------------------------------------------------------------------------- /examples/complex-scene/src/main.rs: -------------------------------------------------------------------------------- 1 | use hotham::HothamResult; 2 | 3 | fn main() -> HothamResult<()> { 4 | complex_scene_example::real_main() 5 | } 6 | -------------------------------------------------------------------------------- /examples/crab-saber/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/crab-saber/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | license = "MIT OR Apache-2.0" 4 | name = "crab-saber" 5 | version = "0.2.0" 6 | 7 | [lib] 8 | crate-type = ["lib", "cdylib"] 9 | 10 | [[bin]] 11 | name = "hotham_crab_saber" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | hotham = {path = "../../hotham"} 16 | rand = "0.8.0" 17 | 18 | [dev-dependencies] 19 | approx = "0.5" 20 | 21 | [target.'cfg(target_os = "android")'.dependencies] 22 | ndk-glue = "0.6" 23 | 24 | [package.metadata.android] 25 | apk_label = "Crab Saber" 26 | fullscreen = true 27 | runtime_libs = "../common_lib" 28 | target_sdk_version = 29 29 | 30 | [[package.metadata.android.uses_permission]] 31 | name = "android.permission.INTERNET" 32 | 33 | [[package.metadata.android.uses_permission]] 34 | name = "android.permission.ACCESS_NETWORK_STATE" 35 | 36 | [package.metadata.android.application] 37 | debuggable = false 38 | label = "Crab Saber" 39 | theme = "@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen" 40 | 41 | [package.metadata.android.application.activity] 42 | config_changes = "screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode" 43 | launch_mode = "singleTask" 44 | orientation = "landscape" 45 | 46 | [[package.metadata.android.application.meta_data]] 47 | name = "com.oculus.supportedDevices" 48 | value = "quest|quest2" 49 | 50 | [[package.metadata.android.application.meta_data]] 51 | name = "com.oculus.intent.category.VR" 52 | value = "vr_only" 53 | 54 | [[package.metadata.android.application.activity.intent_filter]] 55 | actions = ["android.intent.action.MAIN"] 56 | categories = ["com.oculus.intent.category.VR", "android.intent.category.LAUNCHER"] 57 | 58 | [[package.metadata.android.application.activity.meta_data]] 59 | name = "com.oculus.vr.focusaware" 60 | value = "true" 61 | 62 | [[package.metadata.android.uses_feature]] 63 | name = "android.hardware.vulkan.level" 64 | required = true 65 | version = 1 66 | 67 | [[package.metadata.android.uses_feature]] 68 | name = "android.hardware.vr.headtracking" 69 | required = true 70 | version = 1 71 | 72 | # !! IMPORTANT !! 73 | # 74 | # When creating your own apps, make sure to generate your own keystore, rather than using our example one! 75 | # You can use `keytool` like so: 76 | # keytool -genkey -v -keystore my-release-key.keystore -keyalg RSA -keysize 2048 -validity 10000 77 | # 78 | # For more information on key signing and why it's so important, check out this article: 79 | # https://developer.android.com/studio/publish/app-signing 80 | # 81 | # !! IMPORTANT !! 82 | [package.metadata.android.signing.release] 83 | path = "../hotham_examples.keystore" 84 | keystore_password = "chomsky-vigilant-spa" 85 | -------------------------------------------------------------------------------- /examples/crab-saber/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | This example is a clone of the popular VR game, [Beat Saber](https://beatsaber.com/). We're not lawyers, but this is provided as an example *only*: trying to upload this game to an app store is probably not a good idea. 3 | 4 | # Running the example 5 | ## Pre-requisites 6 | 1. [Git LFS](https://git-lfs.github.com/) needs to be enabled for this repo. You'll have a _real_ bad time if you don't. 7 | 1. Install [Rust](https://www.rust-lang.org/tools/install), [cargo apk](https://crates.io/crates/cargo-apk) and the [Android SDK and Android NDK](https://developer.android.com/studio). _Note: only the Android NDK and SDK are required - if Android Studio is not your cup of tea, you can certainly install those separately_ 8 | 1. An Oculus developer account is required & [developer mode](https://developer.oculus.com/documentation/native/android/mobile-device-setup/) needs to be enabled. 9 | 10 | With all that done, you should now be able to see your device from ADB: 11 | 12 | ```` 13 | user@host:~/hotham$ adb devices 14 | List of devices attached 15 | 1PASH9B12E0092 device 16 | ```` 17 | 18 | Then run `run_on_device.ps1` (Windows) or `run_on_device.sh` (Linux) and you're good to go. 19 | 20 | # Troubleshooting 21 | This is definitely _cutting edge_ software, so don't be surprised if it breaks. If you run into any trouble, your friends at the [Hotham discord](https://discord.gg/SZEZUX6ZsQ) can give you a hand! 22 | 23 | # License Information 24 | This example uses a modified version of the asset [Beat Sabers](https://sketchfab.com/3d-models/beat-sabers-e7c6358273d44faea03fa77d9792fd6a), by "Spark", under the [CC4.0 license](https://creativecommons.org/licenses/by/4.0/). 25 | -------------------------------------------------------------------------------- /examples/crab-saber/assets/Chasms - Dark Matter.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3300a21ee310faa72fb1b437247618df5a2644b8b034321433826394a405a1b5 3 | size 7046829 4 | -------------------------------------------------------------------------------- /examples/crab-saber/assets/Hit.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c5185f3662887e7205e36a98d085ffbfc60df2241dcc8eabb27a05f942d44f29 3 | size 39773 4 | -------------------------------------------------------------------------------- /examples/crab-saber/assets/Miss.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9ad8da06bbd7ee1abbdbe5e6c4bef87a79a9d4fb1f710a97f6aef8b4363dbce4 3 | size 31422 4 | -------------------------------------------------------------------------------- /examples/crab-saber/assets/NEFFEX - Tell Me That I Can't.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0c5052949e3bce202cabb5a7ffffb3e8f5f91698f7112206debdf023b2217d5c 3 | size 7065637 4 | -------------------------------------------------------------------------------- /examples/crab-saber/assets/Spence - Right Here Beside You.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c58c6f8cba332b5ad7f0d8fa7a40ba8bfe6d58fa1cb2edd2637e03376d997e0f 3 | size 6977865 4 | -------------------------------------------------------------------------------- /examples/crab-saber/assets/TrackTribe - Cloud Echo.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:79fca3fd88fbabde1969ab15435d1a0627ad31c471b60bbf82ae788076196506 3 | size 6490943 4 | -------------------------------------------------------------------------------- /examples/crab-saber/assets/crab_saber.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7ef784fb740e2583eceacfb885ac17b58fb47c6f209b3d962b321026d71ede37 3 | size 89840 4 | -------------------------------------------------------------------------------- /examples/crab-saber/openxr_loader.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/examples/crab-saber/openxr_loader.dll -------------------------------------------------------------------------------- /examples/crab-saber/scripts/run_on_device.ps1: -------------------------------------------------------------------------------- 1 | adb shell am force-stop rust.crab_saber 2 | 3 | Set-Location $PSScriptRoot\.. 4 | cargo apk run --release 5 | 6 | if ($?) { 7 | $processId = $null 8 | foreach ($i in 1..5) { 9 | $processId = adb shell pidof rust.crab_saber 10 | if ($processId) { break } 11 | Write-Output "Waiting for process to start, sleeping..." 12 | Start-Sleep -Seconds 1 13 | } 14 | if ($processId) { 15 | Write-Output "Found PID of " $processId 16 | adb logcat --pid=$processId 17 | } else { 18 | Write-Error "Failed to find PID of rust.crab_saber" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/crab-saber/scripts/run_on_device.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | adb shell am force-stop rust.crab_saber 5 | 6 | scriptdir=$(dirname -- "$(realpath -- "$0")") 7 | cd $scriptdir/.. 8 | 9 | cargo apk run --release --package crab-saber 10 | 11 | # Wait for the app to start 12 | for i in 1 2 3 4 5; do 13 | adb shell pidof rust.crab_saber && break 14 | sleep 1 15 | done 16 | 17 | adb logcat --pid="$(adb shell pidof rust.crab_saber)" 18 | -------------------------------------------------------------------------------- /examples/crab-saber/src/components/color.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, PartialEq, Eq, Copy)] 2 | pub enum Color { 3 | Red, 4 | Blue, 5 | } 6 | -------------------------------------------------------------------------------- /examples/crab-saber/src/components/cube.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct Cube {} 3 | -------------------------------------------------------------------------------- /examples/crab-saber/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | mod color; 2 | mod cube; 3 | mod saber; 4 | 5 | pub use color::Color; 6 | pub use cube::Cube; 7 | pub use saber::Saber; 8 | -------------------------------------------------------------------------------- /examples/crab-saber/src/components/saber.rs: -------------------------------------------------------------------------------- 1 | pub struct Saber {} 2 | -------------------------------------------------------------------------------- /examples/crab-saber/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod components; 2 | mod game_context; 3 | mod systems; 4 | 5 | use hotham::{ 6 | components::Visible, 7 | hecs::{Entity, World}, 8 | systems::{ 9 | audio_system, draw_gui_system, haptics_system, physics_system, pointers_system, 10 | rendering_system, update_global_transform_system, 11 | }, 12 | xr::{self, SessionState}, 13 | Engine, HothamResult, TickData, 14 | }; 15 | 16 | use game_context::{GameContext, GameState}; 17 | use systems::{game::game_system, sabers_system}; 18 | 19 | #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] 20 | pub fn main() { 21 | println!("[CRAB_SABER] MAIN!"); 22 | real_main().expect("[CRAB_SABER] ERROR IN MAIN!"); 23 | } 24 | 25 | pub fn real_main() -> HothamResult<()> { 26 | let mut engine = Engine::new(); 27 | let mut game_context = init(&mut engine); 28 | 29 | while let Ok(tick_data) = engine.update() { 30 | tick(tick_data, &mut engine, &mut game_context); 31 | engine.finish()?; 32 | } 33 | 34 | Ok(()) 35 | } 36 | 37 | fn tick(tick_data: TickData, engine: &mut Engine, game_context: &mut GameContext) { 38 | handle_state_change(&tick_data, engine, game_context); 39 | 40 | // Simulation tasks - these are only necessary in the focussed state. 41 | if tick_data.current_state == xr::SessionState::FOCUSED { 42 | // Sync world with input contexts 43 | sabers_system(engine); 44 | pointers_system(engine); 45 | 46 | // Update physics simulation 47 | physics_system(engine); 48 | 49 | // Update game simulation 50 | game_system(engine, game_context); 51 | 52 | // Update world 53 | update_global_transform_system(engine); 54 | 55 | // Sync world with output contexts 56 | haptics_system(engine); 57 | audio_system(engine); 58 | draw_gui_system(engine); 59 | } 60 | 61 | // Draw objects 62 | rendering_system(engine, tick_data.swapchain_image_index); 63 | } 64 | 65 | fn handle_state_change(tick_data: &TickData, engine: &mut Engine, game_context: &mut GameContext) { 66 | let world = &mut engine.world; 67 | let audio_context = &mut engine.audio_context; 68 | match (tick_data.previous_state, tick_data.current_state) { 69 | (SessionState::VISIBLE, SessionState::FOCUSED) => { 70 | audio_context.resume_music_track(); 71 | match game_context.state { 72 | GameState::Init => {} 73 | GameState::MainMenu | GameState::GameOver => { 74 | show(world, game_context.pointer); 75 | } 76 | GameState::Playing(_) => { 77 | show(world, game_context.blue_saber); 78 | show(world, game_context.red_saber); 79 | } 80 | } 81 | } 82 | (SessionState::FOCUSED, SessionState::VISIBLE) => { 83 | audio_context.pause_music_track(); 84 | match game_context.state { 85 | GameState::Init => {} 86 | GameState::MainMenu | GameState::GameOver => { 87 | hide(world, game_context.pointer); 88 | } 89 | GameState::Playing(_) => { 90 | hide(world, game_context.blue_saber); 91 | hide(world, game_context.red_saber); 92 | } 93 | } 94 | } 95 | _ => {} 96 | } 97 | } 98 | 99 | fn init(engine: &mut Engine) -> GameContext { 100 | let mut game_context = GameContext::new(engine); 101 | game_context.add_songs(&mut engine.audio_context); 102 | game_context.add_sound_effects(&mut engine.audio_context); 103 | game_context 104 | } 105 | 106 | fn hide(world: &mut World, entity: Entity) { 107 | if world.remove_one::(entity).is_err() { 108 | println!("[STATE_CHANGE] Tried to make {entity:?} hidden but it had no Visible component") 109 | } 110 | } 111 | 112 | fn show(world: &mut World, entity: Entity) { 113 | world.insert_one(entity, Visible {}).unwrap(); 114 | } 115 | -------------------------------------------------------------------------------- /examples/crab-saber/src/main.rs: -------------------------------------------------------------------------------- 1 | use crab_saber::real_main; 2 | use hotham::HothamResult; 3 | 4 | fn main() -> HothamResult<()> { 5 | real_main() 6 | } 7 | -------------------------------------------------------------------------------- /examples/crab-saber/src/systems/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod game; 2 | pub mod sabers; 3 | pub use game::game_system; 4 | pub use sabers::sabers_system; 5 | -------------------------------------------------------------------------------- /examples/crab-saber/src/systems/sabers.rs: -------------------------------------------------------------------------------- 1 | use hotham::{ 2 | asset_importer::{add_model_to_world, Models}, 3 | components::{ 4 | physics::{BodyType, SharedShape}, 5 | stage, Collider, LocalTransform, RigidBody, 6 | }, 7 | contexts::InputContext, 8 | glam::Affine3A, 9 | hecs::{Entity, With, World}, 10 | systems::pointers::{POSITION_OFFSET, ROTATION_OFFSET}, 11 | Engine, 12 | }; 13 | 14 | use crate::components::{Color, Saber}; 15 | 16 | const SABER_HEIGHT: f32 = 0.8; 17 | const SABER_HALF_HEIGHT: f32 = SABER_HEIGHT / 2.; 18 | const SABER_WIDTH: f32 = 0.02; 19 | const SABER_HALF_WIDTH: f32 = SABER_WIDTH / 2.; 20 | 21 | /// Sync the transform of the player's sabers with the pose of their controllers in OpenXR 22 | pub fn sabers_system(engine: &mut Engine) { 23 | sabers_system_inner(&mut engine.world, &engine.input_context) 24 | } 25 | 26 | fn sabers_system_inner(world: &mut World, input_context: &InputContext) { 27 | // Get the isometry of the stage 28 | let global_from_stage = stage::get_global_from_stage(world); 29 | 30 | // Create a transform from local space to grip space. 31 | let grip_from_local = Affine3A::from_rotation_translation(ROTATION_OFFSET, POSITION_OFFSET); 32 | 33 | for (_, (color, local_transform)) in 34 | world.query_mut::>() 35 | { 36 | // Get our the space and path of the hand. 37 | let stage_from_grip = match color { 38 | Color::Red => input_context.left.stage_from_grip(), 39 | Color::Blue => input_context.right.stage_from_grip(), 40 | }; 41 | 42 | // Apply transform 43 | let global_from_local = global_from_stage * stage_from_grip * grip_from_local; 44 | local_transform.update_from_affine(&global_from_local); 45 | } 46 | } 47 | 48 | pub fn add_saber(color: Color, models: &Models, world: &mut World) -> Entity { 49 | let model_name = match color { 50 | Color::Blue => "Blue Saber", 51 | Color::Red => "Red Saber", 52 | }; 53 | let saber = add_model_to_world(model_name, models, world, None).unwrap(); 54 | add_saber_physics(world, saber); 55 | world.insert(saber, (Saber {}, color)).unwrap(); 56 | saber 57 | } 58 | 59 | fn add_saber_physics(world: &mut World, saber: Entity) { 60 | // Give it a collider and rigid-body 61 | let collider = Collider { 62 | shape: SharedShape::cylinder(SABER_HALF_HEIGHT, SABER_HALF_WIDTH), 63 | sensor: true, 64 | ..Default::default() 65 | }; 66 | let rigid_body = RigidBody { 67 | body_type: BodyType::KinematicPositionBased, 68 | ..Default::default() 69 | }; 70 | 71 | // Add the components to the entity. 72 | world.insert(saber, (collider, rigid_body)).unwrap(); 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use super::*; 78 | 79 | #[test] 80 | fn test_sabers() { 81 | use hotham::components::{GlobalTransform, LocalTransform}; 82 | 83 | let mut world = World::new(); 84 | let input_context = InputContext::testing(); 85 | let saber = world.spawn(( 86 | Color::Red, 87 | Saber {}, 88 | LocalTransform::default(), 89 | GlobalTransform::default(), 90 | )); 91 | sabers_system_inner(&mut world, &input_context); 92 | 93 | let local_transform = world.get::<&LocalTransform>(saber).unwrap(); 94 | approx::assert_relative_eq!( 95 | local_transform.translation, 96 | [-0.2, 1.3258567, -0.47001815].into() 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /examples/custom-rendering/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | license = "MIT OR Apache-2.0" 4 | name = "custom-rendering-example" 5 | version = "0.2.0" 6 | 7 | [lib] 8 | crate-type = ["lib", "cdylib"] 9 | 10 | [[bin]] 11 | name = "hotham_custom_rendering_example" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | hotham = {path = "../../hotham"} 16 | hotham-examples = {path = "../shared"} 17 | 18 | [target.'cfg(target_os = "android")'.dependencies] 19 | ndk-glue = "0.6" 20 | 21 | [package.metadata.android] 22 | apk_label = "Hotham Custom Rendering Example" 23 | fullscreen = true 24 | runtime_libs = "../common_lib" 25 | target_sdk_version = 29 26 | 27 | [package.metadata.android.application] 28 | debuggable = true 29 | label = "Hotham Custom Rendering Example" 30 | theme = "@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen" 31 | 32 | [package.metadata.android.application.activity] 33 | config_changes = "screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode" 34 | launch_mode = "singleTask" 35 | orientation = "landscape" 36 | 37 | [[package.metadata.android.uses_permission]] 38 | name = "android.permission.INTERNET" 39 | 40 | [[package.metadata.android.uses_permission]] 41 | name = "android.permission.access_network_state" 42 | 43 | [[package.metadata.android.application.meta_data]] 44 | name = "com.oculus.supportedDevices" 45 | value = "quest|quest2" 46 | 47 | [[package.metadata.android.application.meta_data]] 48 | name = "com.oculus.intent.category.VR" 49 | value = "vr_only" 50 | 51 | [[package.metadata.android.application.activity.intent_filter]] 52 | actions = ["android.intent.action.MAIN"] 53 | categories = ["com.oculus.intent.category.VR", "android.intent.category.LAUNCHER"] 54 | 55 | [[package.metadata.android.application.activity.meta_data]] 56 | name = "com.oculus.vr.focusaware" 57 | value = "true" 58 | 59 | [[package.metadata.android.uses_feature]] 60 | name = "android.hardware.vulkan.level" 61 | required = true 62 | version = 1 63 | 64 | [[package.metadata.android.uses_feature]] 65 | name = "android.hardware.vr.headtracking" 66 | required = true 67 | version = 1 68 | 69 | # !! IMPORTANT !! 70 | # 71 | # When creating your own apps, make sure to generate your own keystore, rather than using our example one! 72 | # You can use `keytool` like so: 73 | # keytool -genkey -v -keystore my-release-key.keystore -keyalg RSA -keysize 2048 -validity 10000 74 | # 75 | # For more information on key signing and why it's so important, check out this article: 76 | # https://developer.android.com/studio/publish/app-signing 77 | # 78 | # !! IMPORTANT !! 79 | [package.metadata.android.signing.release] 80 | path = "../hotham_examples.keystore" 81 | keystore_password = "chomsky-vigilant-spa" 82 | -------------------------------------------------------------------------------- /examples/custom-rendering/README.md: -------------------------------------------------------------------------------- 1 | # Custom Rendering 2 | This example shows how to perform custom rendering with hotham. 3 | -------------------------------------------------------------------------------- /examples/custom-rendering/openxr_loader.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/examples/custom-rendering/openxr_loader.dll -------------------------------------------------------------------------------- /examples/custom-rendering/scripts/run_on_device.ps1: -------------------------------------------------------------------------------- 1 | adb shell am force-stop rust.custom_rendering_example 2 | 3 | Set-Location $PSScriptRoot\.. 4 | cargo apk run --release 5 | 6 | if ($?) { 7 | Start-Sleep -Seconds 2 8 | $processIdStr = (adb shell pidof rust.custom_rendering_example) | Out-String 9 | Write-Output $processIdStr 10 | $processId = $processIdStr -as [int] 11 | Write-Output $processId 12 | adb logcat --pid=$processId 13 | } 14 | -------------------------------------------------------------------------------- /examples/custom-rendering/scripts/run_on_device.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | adb shell am force-stop rust.custom_rendering_example 5 | 6 | scriptdir=$(dirname -- "$(realpath -- "$0")") 7 | cd $scriptdir/.. 8 | 9 | cargo apk run --release 10 | 11 | # Wait for the app to start 12 | for i in 1 2 3 4 5; do 13 | adb shell pidof rust.custom_rendering_example && break 14 | sleep 1 15 | done 16 | 17 | adb logcat --pid="$(adb shell pidof rust.custom_rendering_example)" 18 | -------------------------------------------------------------------------------- /examples/custom-rendering/src/hologram.rs: -------------------------------------------------------------------------------- 1 | use hotham::{glam::Mat4, id_arena::Id, rendering::mesh_data::MeshData}; 2 | 3 | /// The Hologram component is used together with custom rendering to render quadric surfaces. 4 | #[derive(Debug, Clone, Copy)] 5 | pub struct Hologram { 6 | pub mesh_data_handle: Id, 7 | pub hologram_data: HologramData, 8 | } 9 | 10 | #[derive(Debug, Clone, Copy)] 11 | pub struct HologramData { 12 | pub surface_q_in_local: Mat4, 13 | pub bounds_q_in_local: Mat4, 14 | pub uv_from_local: Mat4, 15 | } 16 | -------------------------------------------------------------------------------- /examples/custom-rendering/src/main.rs: -------------------------------------------------------------------------------- 1 | use hotham::HothamResult; 2 | 3 | fn main() -> HothamResult<()> { 4 | custom_rendering_example::real_main() 5 | } 6 | -------------------------------------------------------------------------------- /examples/custom-rendering/src/shaders/quadric.glsl: -------------------------------------------------------------------------------- 1 | struct QuadricData { 2 | mat4 gosFromLocal; 3 | mat4 surfaceQ; 4 | mat4 boundsQ; 5 | mat4 uvFromGos; 6 | }; 7 | 8 | layout (std430, set = 1, binding = 0) readonly buffer QuadricDataBuffer { 9 | QuadricData data[]; 10 | } quadricDataBuffer; 11 | -------------------------------------------------------------------------------- /examples/custom-rendering/src/shaders/quadric.vert: -------------------------------------------------------------------------------- 1 | #version 460 2 | 3 | #include "../../../../hotham/src/shaders/common.glsl" 4 | 5 | layout (location = 0) in vec3 inPos; 6 | 7 | layout (location = 0) out vec4 outGosPos; 8 | layout (location = 1) out flat uint outInstanceIndex; 9 | 10 | #include "quadric.glsl" 11 | 12 | out gl_PerVertex { 13 | vec4 gl_Position; 14 | }; 15 | 16 | void main() { 17 | QuadricData d = quadricDataBuffer.data[gl_InstanceIndex]; 18 | outInstanceIndex = gl_InstanceIndex; 19 | outGosPos = d.gosFromLocal * vec4(inPos, 1.0); 20 | gl_Position = sceneData.viewProjection[gl_ViewIndex] * outGosPos; 21 | } 22 | -------------------------------------------------------------------------------- /examples/custom-rendering/src/surface_solver.rs: -------------------------------------------------------------------------------- 1 | use hotham::{ 2 | components::LocalTransform, 3 | glam::{Mat4, Vec3}, 4 | hecs::{Entity, World}, 5 | na::{ArrayStorage, Matrix, U1, U10, U3}, 6 | util::na_vector_from_glam, 7 | Engine, 8 | }; 9 | 10 | use crate::hologram::Hologram; 11 | 12 | type Matrix10x10 = Matrix>; 13 | type Matrix10x3 = Matrix>; 14 | type Vector10 = Matrix>; 15 | type RowVector10 = Matrix>; 16 | 17 | pub struct ControlPoints { 18 | pub entities: Vec, 19 | } 20 | 21 | pub struct HologramBackside { 22 | pub entity: Entity, 23 | } 24 | 25 | pub fn surface_solver_system(engine: &mut Engine) { 26 | let world = &mut engine.world; 27 | surface_solver_system_inner(world); 28 | } 29 | 30 | fn surface_solver_system_inner(world: &mut World) { 31 | for (_, (hologram, control_points, local_transform)) in world 32 | .query::<(&mut Hologram, &mut ControlPoints, &mut LocalTransform)>() 33 | .iter() 34 | { 35 | let local_from_global = local_transform.to_affine().inverse(); 36 | 37 | #[allow(non_snake_case)] 38 | let mut AtA: Matrix10x10 = Default::default(); 39 | #[allow(non_snake_case)] 40 | let mut BtB: Matrix10x10 = Default::default(); 41 | #[allow(non_snake_case)] 42 | let mut BtN: Vector10 = Default::default(); 43 | 44 | for e in &control_points.entities { 45 | let t = world.get::<&LocalTransform>(*e).unwrap(); 46 | let global_from_control = t.to_affine(); 47 | let local_from_control = local_from_global * global_from_control; 48 | let point_in_local = local_from_control.transform_point3(Vec3::ZERO); 49 | let arrow_in_local = local_from_control.transform_vector3(Vec3::Y); 50 | let p = na_vector_from_glam(point_in_local); 51 | let d = na_vector_from_glam(arrow_in_local); 52 | 53 | let (x, y, z) = (p.x, p.y, p.z); 54 | let a_row: RowVector10 = 55 | [x * x, y * y, z * z, x * y, x * z, y * z, x, y, z, 1.0].into(); 56 | AtA += a_row.transpose() * a_row; 57 | let b_rows_t = Matrix10x3::from_columns(&[ 58 | [2.0 * x, 0.0, 0.0, y, z, 0.0, 1.0, 0.0, 0.0, 0.].into(), 59 | [0.0, 2.0 * y, 0.0, x, 0.0, z, 0.0, 1.0, 0.0, 0.].into(), 60 | [0.0, 0.0, 2.0 * z, 0.0, x, y, 0.0, 0.0, 1.0, 0.].into(), 61 | ]); 62 | BtB += b_rows_t * b_rows_t.transpose(); 63 | BtN += b_rows_t * d; 64 | } 65 | // let eigen_decomposition = AtA.symmetric_eigen(); 66 | // eigen_decomposition 67 | let eps = 1.0e-6; 68 | let svd = AtA.svd(false, true); 69 | let rank = svd.rank(eps).min(9); 70 | let v_t = svd.v_t.unwrap(); 71 | let nullspace = v_t.rows(rank, 10 - rank); 72 | let svd_subspace = (nullspace * BtB * nullspace.transpose()).svd(true, true); 73 | let solution_in_subspace = svd_subspace.pseudo_inverse(eps).unwrap() * nullspace * BtN; 74 | let q = nullspace.transpose() * solution_in_subspace; 75 | hologram.hologram_data.surface_q_in_local = Mat4::from_cols_array_2d(&[ 76 | [2.0 * q[0], q[3], q[4], q[6]], 77 | [q[3], 2.0 * q[1], q[5], q[7]], 78 | [q[4], q[5], 2.0 * q[2], q[8]], 79 | [q[6], q[7], q[8], 2.0 * q[9]], 80 | ]); 81 | } 82 | 83 | for (_, (target, source)) in world.query::<(&mut Hologram, &HologramBackside)>().iter() { 84 | target.hologram_data.surface_q_in_local = world 85 | .get::<&Hologram>(source.entity) 86 | .unwrap() 87 | .hologram_data 88 | .surface_q_in_local 89 | * -1.0; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /examples/hotham_examples.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/examples/hotham_examples.keystore -------------------------------------------------------------------------------- /examples/shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | license = "MIT OR Apache-2.0" 4 | name = "hotham-examples" 5 | version = "0.2.0" 6 | 7 | [dependencies] 8 | hotham = {path = "../../hotham"} 9 | -------------------------------------------------------------------------------- /examples/shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod navigation; 2 | -------------------------------------------------------------------------------- /examples/simple-scene/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2018" 3 | license = "MIT OR Apache-2.0" 4 | name = "simple-scene-example" 5 | version = "0.2.0" 6 | 7 | [lib] 8 | crate-type = ["lib", "cdylib"] 9 | 10 | [[bin]] 11 | name = "hotham_simple_scene_example" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | hotham = {path = "../../hotham"} 16 | 17 | [target.'cfg(target_os = "android")'.dependencies] 18 | ndk-glue = "0.6" 19 | 20 | [package.metadata.android] 21 | apk_label = "Hotham Simple Scene Example" 22 | fullscreen = true 23 | runtime_libs = "../common_lib" 24 | target_sdk_version = 29 25 | 26 | [package.metadata.android.application] 27 | debuggable = true 28 | label = "Hotham Simple Scene Example" 29 | theme = "@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen" 30 | 31 | [package.metadata.android.application.activity] 32 | config_changes = "screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode" 33 | launch_mode = "singleTask" 34 | orientation = "landscape" 35 | 36 | [[package.metadata.android.uses_permission]] 37 | name = "android.permission.INTERNET" 38 | 39 | [[package.metadata.android.uses_permission]] 40 | name = "android.permission.ACCESS_NETWORK_STATE" 41 | 42 | [[package.metadata.android.application.meta_data]] 43 | name = "com.oculus.supportedDevices" 44 | value = "quest|quest2" 45 | 46 | [[package.metadata.android.application.meta_data]] 47 | name = "com.oculus.intent.category.VR" 48 | value = "vr_only" 49 | 50 | [[package.metadata.android.application.activity.intent_filter]] 51 | actions = ["android.intent.action.MAIN"] 52 | categories = ["com.oculus.intent.category.VR", "android.intent.category.LAUNCHER"] 53 | 54 | [[package.metadata.android.application.activity.meta_data]] 55 | name = "com.oculus.vr.focusaware" 56 | value = "true" 57 | 58 | [[package.metadata.android.uses_feature]] 59 | name = "android.hardware.vulkan.level" 60 | required = true 61 | version = 1 62 | 63 | [[package.metadata.android.uses_feature]] 64 | name = "android.hardware.vr.headtracking" 65 | required = true 66 | version = 1 67 | 68 | # !! IMPORTANT !! 69 | # 70 | # When creating your own apps, make sure to generate your own keystore, rather than using our example one! 71 | # You can use `keytool` like so: 72 | # keytool -genkey -v -keystore my-release-key.keystore -keyalg RSA -keysize 2048 -validity 10000 73 | # 74 | # For more information on key signing and why it's so important, check out this article: 75 | # https://developer.android.com/studio/publish/app-signing 76 | # 77 | # !! IMPORTANT !! 78 | [package.metadata.android.signing.release] 79 | path = "../hotham_examples.keystore" 80 | keystore_password = "chomsky-vigilant-spa" 81 | -------------------------------------------------------------------------------- /examples/simple-scene/README.md: -------------------------------------------------------------------------------- 1 | ![Asteroid Logo](logo.png?raw=true) 2 | # Asteroid 3 | Asteroid is a simple game designed to show off Hotham's capabilities. 4 | 5 | The premise of the game is simple: defend an asteroid and its refining facility from alien invasion. 6 | -------------------------------------------------------------------------------- /examples/simple-scene/logo.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0b8879e7a996b0a7727bab4367878bd9d18aaaf6f8a2c343bd5622a406f4c559 3 | size 583949 4 | -------------------------------------------------------------------------------- /examples/simple-scene/openxr_loader.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/examples/simple-scene/openxr_loader.dll -------------------------------------------------------------------------------- /examples/simple-scene/scripts/run_on_device.ps1: -------------------------------------------------------------------------------- 1 | adb shell am force-stop rust.simple_scene_example 2 | 3 | Set-Location $PSScriptRoot\.. 4 | cargo apk run --release 5 | 6 | if ($?) { 7 | $processId = $null 8 | foreach ($i in 1..5) { 9 | $processId = adb shell pidof rust.simple_scene_example 10 | if ($processId) { break } 11 | Write-Output "Waiting for process to start, sleeping..." 12 | Start-Sleep -Seconds 1 13 | } 14 | if ($processId) { 15 | Write-Output "Found PID of " $processId 16 | adb logcat --pid=$processId 17 | } else { 18 | Write-Error "Failed to find PID of rust.simple_scene_example" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/simple-scene/scripts/run_on_device.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | adb shell am force-stop rust.simple_scene_example 5 | 6 | scriptdir=$(dirname -- "$(realpath -- "$0")") 7 | cd $scriptdir/.. 8 | 9 | cargo apk run --release 10 | 11 | # Wait for the app to start 12 | for i in 1 2 3 4 5; do 13 | adb shell pidof rust.simple_scene_example && break 14 | sleep 1 15 | done 16 | 17 | adb logcat --pid="$(adb shell pidof rust.simple_scene_example)" 18 | -------------------------------------------------------------------------------- /examples/simple-scene/src/lib.rs: -------------------------------------------------------------------------------- 1 | use hotham::{ 2 | asset_importer::{self, add_model_to_world}, 3 | components::{ 4 | hand::Handedness, 5 | physics::{BodyType, SharedShape}, 6 | Collider, LocalTransform, RigidBody, 7 | }, 8 | hecs::World, 9 | na, 10 | systems::{ 11 | animation_system, debug::debug_system, grabbing_system, hands::add_hand, hands_system, 12 | physics_system, rendering::rendering_system, skinning::skinning_system, 13 | update_global_transform_system, 14 | }, 15 | xr, Engine, HothamResult, TickData, 16 | }; 17 | 18 | #[derive(Clone, Debug, Default)] 19 | /// Most Hotham applications will want to keep track of some sort of state. 20 | /// However, this _simple_ scene doesn't have any, so this is just left here to let you know that 21 | /// this is something you'd probably want to do! 22 | struct State {} 23 | 24 | #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] 25 | pub fn main() { 26 | println!("[HOTHAM_SIMPLE_SCENE] MAIN!"); 27 | real_main().expect("Error running app!"); 28 | println!("[HOTHAM_SIMPLE_SCENE] FINISHED! Goodbye!"); 29 | } 30 | 31 | pub fn real_main() -> HothamResult<()> { 32 | let mut engine = Engine::new(); 33 | let mut state = Default::default(); 34 | init(&mut engine)?; 35 | 36 | while let Ok(tick_data) = engine.update() { 37 | tick(tick_data, &mut engine, &mut state); 38 | engine.finish()?; 39 | } 40 | 41 | Ok(()) 42 | } 43 | 44 | fn tick(tick_data: TickData, engine: &mut Engine, _state: &mut State) { 45 | if tick_data.current_state == xr::SessionState::FOCUSED { 46 | hands_system(engine); 47 | grabbing_system(engine); 48 | physics_system(engine); 49 | animation_system(engine); 50 | update_global_transform_system(engine); 51 | skinning_system(engine); 52 | debug_system(engine); 53 | } 54 | 55 | rendering_system(engine, tick_data.swapchain_image_index); 56 | } 57 | 58 | fn init(engine: &mut Engine) -> Result<(), hotham::HothamError> { 59 | let render_context = &mut engine.render_context; 60 | let vulkan_context = &mut engine.vulkan_context; 61 | let world = &mut engine.world; 62 | 63 | let mut glb_buffers: Vec<&[u8]> = vec![ 64 | include_bytes!("../../../test_assets/floor.glb"), 65 | include_bytes!("../../../test_assets/left_hand.glb"), 66 | include_bytes!("../../../test_assets/right_hand.glb"), 67 | ]; 68 | let models = 69 | asset_importer::load_models_from_glb(&glb_buffers, vulkan_context, render_context)?; 70 | add_floor(&models, world); 71 | add_hand(&models, Handedness::Left, world); 72 | add_hand(&models, Handedness::Right, world); 73 | 74 | #[cfg(target_os = "android")] 75 | glb_buffers.push(include_bytes!( 76 | "../../../test_assets/damaged_helmet_squished.glb" 77 | )); 78 | 79 | #[cfg(not(target_os = "android"))] 80 | glb_buffers.push(include_bytes!("../../../test_assets/damaged_helmet.glb")); 81 | 82 | let models = 83 | asset_importer::load_models_from_glb(&glb_buffers, vulkan_context, render_context)?; 84 | add_helmet(&models, world); 85 | add_model_to_world("Cube", &models, world, None); 86 | 87 | // Update global transforms from local transforms before physics_system gets confused 88 | update_global_transform_system(engine); 89 | 90 | Ok(()) 91 | } 92 | 93 | fn add_floor(models: &std::collections::HashMap, world: &mut World) { 94 | let entity = add_model_to_world("Floor", models, world, None).expect("Could not find Floor"); 95 | let collider = Collider::new(SharedShape::halfspace(na::Vector3::y_axis())); 96 | let rigid_body = RigidBody { 97 | body_type: BodyType::Fixed, 98 | ..Default::default() 99 | }; 100 | world.insert(entity, (collider, rigid_body)).unwrap(); 101 | } 102 | 103 | fn add_helmet(models: &std::collections::HashMap, world: &mut World) { 104 | let helmet = add_model_to_world("Damaged Helmet", models, world, None) 105 | .expect("Could not find Damaged Helmet"); 106 | 107 | { 108 | let mut local_transform = world.get::<&mut LocalTransform>(helmet).unwrap(); 109 | local_transform.translation.z = -1.; 110 | local_transform.translation.y = 1.4; 111 | local_transform.scale = [0.5, 0.5, 0.5].into(); 112 | } 113 | 114 | let collider = Collider::new(SharedShape::ball(0.35)); 115 | 116 | world 117 | .insert(helmet, (collider, RigidBody::default())) 118 | .unwrap(); 119 | } 120 | -------------------------------------------------------------------------------- /examples/simple-scene/src/main.rs: -------------------------------------------------------------------------------- 1 | use hotham::HothamResult; 2 | 3 | fn main() -> HothamResult<()> { 4 | simple_scene_example::real_main() 5 | } 6 | -------------------------------------------------------------------------------- /hotham-asset-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "hotham-asset-client" 4 | version = "0.1.0" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0" 10 | futures-util = {version = "0.3.11", default-features = false} 11 | quinn = {version = "0.8.5", features = ["tls-rustls"]} 12 | rustls = {version = "0.20.3", features = ["dangerous_configuration", "quic"]} 13 | tokio = {version = "1.0.1", default-features = false, features = ["sync"]} 14 | 15 | [dev-dependencies] 16 | tokio = {version = "1.0.1", default-features = false, features = ["rt"]} 17 | -------------------------------------------------------------------------------- /hotham-asset-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod message; 3 | use std::sync::Arc; 4 | 5 | pub use client::watch; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct AssetUpdatedMessage { 9 | pub asset_id: String, 10 | pub asset_data: Arc>, 11 | } 12 | -------------------------------------------------------------------------------- /hotham-asset-client/src/message.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | #[non_exhaustive] 4 | #[derive(Debug, Clone, PartialEq, Eq)] 5 | pub enum MessageType { 6 | GetAsset, 7 | WatchAsset, 8 | AssetUpdated, 9 | OK, 10 | Error, 11 | Asset, 12 | _Invalid, 13 | } 14 | 15 | #[derive(Debug, Clone, PartialEq, Eq)] 16 | pub enum Message<'a> { 17 | GetAsset(&'a str), 18 | WatchAsset(&'a str), 19 | AssetUpdated(&'a str), 20 | OK, 21 | Error(String), 22 | Asset(Vec), 23 | } 24 | 25 | impl<'a> Message<'a> { 26 | pub fn parse(message_type: MessageType, buffer: &'a [u8]) -> Result { 27 | let message = match message_type { 28 | MessageType::GetAsset => Message::GetAsset(std::str::from_utf8(buffer)?), 29 | MessageType::WatchAsset => Message::WatchAsset(std::str::from_utf8(buffer)?), 30 | MessageType::AssetUpdated => Message::AssetUpdated(std::str::from_utf8(buffer)?), 31 | MessageType::OK => Message::OK, 32 | MessageType::Error => Message::Error(std::str::from_utf8(buffer)?.into()), 33 | MessageType::Asset => Message::Asset(buffer.to_vec()), 34 | _ => anyhow::bail!("Invalid message type"), 35 | }; 36 | 37 | Ok(message) 38 | } 39 | 40 | pub async fn read(recv: &mut quinn::RecvStream, buffer: &'a mut [u8]) -> Result> { 41 | // Read 9 bytes 42 | recv.read_exact(&mut buffer[0..9]).await?; 43 | // Byte 0 is the tag: 44 | let message_type: MessageType = unsafe { std::mem::transmute(buffer[0]) }; 45 | // Bytes 1..9 is the message length as big endian u64 46 | let message_length = u64::from_be_bytes(buffer[1..9].try_into()?); 47 | 48 | let message_buffer = &mut buffer[0..message_length as _]; 49 | recv.read_exact(message_buffer).await?; 50 | Message::parse(message_type, message_buffer) 51 | } 52 | 53 | pub async fn write_all(&'a self, stream: &mut quinn::SendStream) -> Result<()> { 54 | // Would it be easier to just put this all into another buffer? 55 | // Yes 56 | // Would it be as fun? 57 | // No. 58 | 59 | // Byte 0 is the tag: 60 | stream.write_all(&[self.get_type() as _]).await?; 61 | // Bytes 1..9 is the message length as big endian u64 62 | stream.write_all(&(self.len() as u64).to_be_bytes()).await?; 63 | stream.write_all(self.buf()).await?; 64 | 65 | Ok(()) 66 | } 67 | 68 | pub fn get_type(&self) -> MessageType { 69 | match self { 70 | Message::GetAsset(_) => MessageType::GetAsset, 71 | Message::WatchAsset(_) => MessageType::WatchAsset, 72 | Message::AssetUpdated(_) => MessageType::AssetUpdated, 73 | Message::OK => MessageType::OK, 74 | Message::Error(_) => MessageType::Error, 75 | Message::Asset(_) => MessageType::Asset, 76 | } 77 | } 78 | 79 | pub fn len(&self) -> usize { 80 | self.buf().len() 81 | } 82 | 83 | pub fn is_empty(&self) -> bool { 84 | self.buf().is_empty() 85 | } 86 | 87 | pub fn buf(&self) -> &'_ [u8] { 88 | match self { 89 | Message::GetAsset(s) => s.as_bytes(), 90 | Message::WatchAsset(s) => s.as_bytes(), 91 | Message::AssetUpdated(s) => s.as_bytes(), 92 | Message::OK => &[], 93 | Message::Error(s) => s.as_bytes(), 94 | Message::Asset(b) => b, 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /hotham-asset-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "hotham-asset-server" 4 | version = "0.1.0" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0" 10 | futures-util = {version = "0.3.11", default-features = false, features = ["io"]} 11 | hotham-asset-client = {path = "../hotham-asset-client"} 12 | notify-debouncer-mini = {version = "*", default-features = false} 13 | quinn = {version = "0.8.5", features = ["tls-rustls"]} 14 | rcgen = "0.10.0" 15 | rustls = {version = "0.20.3", features = ["dangerous_configuration", "quic"]} 16 | tokio = {version = "1.21.2", features = ["full"]} 17 | -------------------------------------------------------------------------------- /hotham-asset-server/src/main.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | 3 | use std::{collections::HashMap, sync::Arc, time::SystemTime}; 4 | use tokio::sync::Mutex; 5 | 6 | use anyhow::Result; 7 | use futures_util::StreamExt; 8 | /// A simple server that serves assets to localhost or remote targets. It's great and has no flaws. 9 | // TODO: 10 | // 1. Accept connections 11 | // 2. Send a file on "GET" 12 | // 3. Watch for file updates 13 | // 4. Send a "file updated" message back to the client on update 14 | // 5. GOTO 2 15 | use server::{handle_connection, make_server_endpoint, watch_files}; 16 | 17 | pub type WatchList = Arc>>>; 18 | 19 | #[tokio::main] 20 | async fn main() -> Result<()> { 21 | let addr = "0.0.0.0:5000".parse().unwrap(); 22 | let (mut incoming, _server_cert) = make_server_endpoint(addr).unwrap(); 23 | 24 | loop { 25 | let incoming_conn = incoming.next().await.unwrap(); 26 | let new_conn = incoming_conn.await.unwrap(); 27 | let watch_list = WatchList::default(); 28 | 29 | let watcher_watch_list = watch_list.clone(); 30 | let watcher_connection = new_conn.connection.clone(); 31 | tokio::spawn(watch_files(watcher_connection, watcher_watch_list)); 32 | tokio::spawn(handle_connection(new_conn, watch_list.clone())); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /hotham-simulator/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Run tests", 8 | "type": "shell", 9 | "command": "./test.sh", 10 | "group": { 11 | "kind": "test", 12 | "isDefault": true 13 | }, 14 | "presentation": { 15 | "reveal": "always", 16 | "panel": "shared" 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /hotham-simulator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | build = "build.rs" 3 | edition = "2018" 4 | license = "MIT OR Apache-2.0" 5 | name = "hotham-simulator" 6 | version = "0.2.0" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | ash = "0.33.2" 13 | ash-window = "0.7" 14 | glam = "0.23" 15 | lazy_static = "1.4.0" 16 | openxr-sys = "0.9" 17 | rand = "0.8" 18 | vk-shader-macros = "0.2.8" 19 | winit = "0.26" 20 | 21 | [build-dependencies] 22 | bindgen = "*" 23 | -------------------------------------------------------------------------------- /hotham-simulator/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | To make VR development a little bit less painful (_because who really wants to keep taking their headset on and off all the time_), Hotham comes with a handy-dandy OpenXR simulator. 3 | 4 | To get started with the simulator, follow the instructions over [here](https://github.com/leetvr/hotham/wiki/Adding-the-Hotham-Simulator-to-your-development-environment). 5 | -------------------------------------------------------------------------------- /hotham-simulator/build.rs: -------------------------------------------------------------------------------- 1 | // Dummy function 2 | #[cfg(target_os = "android")] 3 | fn main() {} 4 | 5 | #[cfg(not(target_os = "android"))] 6 | fn main() { 7 | println!("cargo:rerun-if-changed=build_input"); 8 | // use std::{fs, path::Path, str}; 9 | // The bindgen::Builder is the main entry point 10 | // to bindgen, and lets you build up options for 11 | // the resulting bindings. 12 | 13 | // Annoying as requires complicated clang setup on windows. Headers are unlikely to change any time soon. 14 | // let bindings = bindgen::Builder::default() 15 | // // The input header we would like to generate 16 | // // bindings for. 17 | // .header("build_input/wrapper.h") 18 | // // Finish the builder and generate the bindings. 19 | // .generate() 20 | // // Unwrap the Result and panic on failure. 21 | // .expect("Unable to generate bindings"); 22 | 23 | // bindings 24 | // .write_to_file("src/bindings.rs") 25 | // .expect("Couldn't write bindings!"); 26 | } 27 | -------------------------------------------------------------------------------- /hotham-simulator/build_input/openxr/openxr_platform_defines.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (c) 2017-2021, The Khronos Group Inc. 3 | ** 4 | ** SPDX-License-Identifier: Apache-2.0 OR MIT 5 | */ 6 | 7 | #ifndef OPENXR_PLATFORM_DEFINES_H_ 8 | #define OPENXR_PLATFORM_DEFINES_H_ 1 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | /* Platform-specific calling convention macros. 15 | * 16 | * Platforms should define these so that OpenXR clients call OpenXR functions 17 | * with the same calling conventions that the OpenXR implementation expects. 18 | * 19 | * XRAPI_ATTR - Placed before the return type in function declarations. 20 | * Useful for C++11 and GCC/Clang-style function attribute syntax. 21 | * XRAPI_CALL - Placed after the return type in function declarations. 22 | * Useful for MSVC-style calling convention syntax. 23 | * XRAPI_PTR - Placed between the '(' and '*' in function pointer types. 24 | * 25 | * Function declaration: XRAPI_ATTR void XRAPI_CALL xrFunction(void); 26 | * Function pointer type: typedef void (XRAPI_PTR *PFN_xrFunction)(void); 27 | */ 28 | #if defined(_WIN32) 29 | #define XRAPI_ATTR 30 | // On Windows, functions use the stdcall convention 31 | #define XRAPI_CALL __stdcall 32 | #define XRAPI_PTR XRAPI_CALL 33 | #elif defined(__ANDROID__) && defined(__ARM_ARCH) && __ARM_ARCH < 7 34 | #error "API not supported for the 'armeabi' NDK ABI" 35 | #elif defined(__ANDROID__) && defined(__ARM_ARCH) && __ARM_ARCH >= 7 && defined(__ARM_32BIT_STATE) 36 | // On Android 32-bit ARM targets, functions use the "hardfloat" 37 | // calling convention, i.e. float parameters are passed in registers. This 38 | // is true even if the rest of the application passes floats on the stack, 39 | // as it does by default when compiling for the armeabi-v7a NDK ABI. 40 | #define XRAPI_ATTR __attribute__((pcs("aapcs-vfp"))) 41 | #define XRAPI_CALL 42 | #define XRAPI_PTR XRAPI_ATTR 43 | #else 44 | // On other platforms, use the default calling convention 45 | #define XRAPI_ATTR 46 | #define XRAPI_CALL 47 | #define XRAPI_PTR 48 | #endif 49 | 50 | #include 51 | 52 | #if !defined(XR_NO_STDINT_H) 53 | #if defined(_MSC_VER) && (_MSC_VER < 1600) 54 | typedef signed __int8 int8_t; 55 | typedef unsigned __int8 uint8_t; 56 | typedef signed __int16 int16_t; 57 | typedef unsigned __int16 uint16_t; 58 | typedef signed __int32 int32_t; 59 | typedef unsigned __int32 uint32_t; 60 | typedef signed __int64 int64_t; 61 | typedef unsigned __int64 uint64_t; 62 | #else 63 | #include 64 | #endif 65 | #endif // !defined( XR_NO_STDINT_H ) 66 | 67 | // XR_PTR_SIZE (in bytes) 68 | #if (defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__) ) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(__powerpc64__)) 69 | #define XR_PTR_SIZE 8 70 | #else 71 | #define XR_PTR_SIZE 4 72 | #endif 73 | 74 | // Needed so we can use clang __has_feature portably. 75 | #if !defined(XR_COMPILER_HAS_FEATURE) 76 | #if defined(__clang__) 77 | #define XR_COMPILER_HAS_FEATURE(x) __has_feature(x) 78 | #else 79 | #define XR_COMPILER_HAS_FEATURE(x) 0 80 | #endif 81 | #endif 82 | 83 | // Identifies if the current compiler has C++11 support enabled. 84 | // Does not by itself identify if any given C++11 feature is present. 85 | #if !defined(XR_CPP11_ENABLED) && defined(__cplusplus) 86 | #if defined(__GNUC__) && defined(__GXX_EXPERIMENTAL_CXX0X__) 87 | #define XR_CPP11_ENABLED 1 88 | #elif defined(_MSC_VER) && (_MSC_VER >= 1600) 89 | #define XR_CPP11_ENABLED 1 90 | #elif (__cplusplus >= 201103L) // 201103 is the first C++11 version. 91 | #define XR_CPP11_ENABLED 1 92 | #endif 93 | #endif 94 | 95 | // Identifies if the current compiler supports C++11 nullptr. 96 | #if !defined(XR_CPP_NULLPTR_SUPPORTED) 97 | #if defined(XR_CPP11_ENABLED) && \ 98 | ((defined(__clang__) && XR_COMPILER_HAS_FEATURE(cxx_nullptr)) || \ 99 | (defined(__GNUC__) && (((__GNUC__ * 1000) + __GNUC_MINOR__) >= 4006)) || \ 100 | (defined(_MSC_VER) && (_MSC_VER >= 1600)) || \ 101 | (defined(__EDG_VERSION__) && (__EDG_VERSION__ >= 403))) 102 | #define XR_CPP_NULLPTR_SUPPORTED 1 103 | #endif 104 | #endif 105 | 106 | #ifdef __cplusplus 107 | } 108 | #endif 109 | 110 | #endif 111 | -------------------------------------------------------------------------------- /hotham-simulator/build_input/viewdisplay.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | #extension GL_EXT_multiview : require 3 | 4 | layout (binding = 0) uniform sampler2DArray samplerView; 5 | 6 | layout (location = 0) in vec2 inUV; 7 | layout (location = 0) out vec4 outColor; 8 | 9 | layout (constant_id = 0) const float VIEW_LAYER = 0.0f; 10 | 11 | void main() 12 | { 13 | const float alpha = 0.0; 14 | 15 | vec2 p1 = vec2(2.0 * inUV - 1.0); 16 | vec2 p2 = p1 / (1.0 - alpha * length(p1)); 17 | p2 = (p2 + 1.0) * 0.5; 18 | 19 | bool inside = ((p2.x >= 0.0) && (p2.x <= 1.0) && (p2.y >= 0.0 ) && (p2.y <= 1.0)); 20 | outColor = inside ? texture(samplerView, vec3(p2, VIEW_LAYER)) : vec4(0.0); 21 | } -------------------------------------------------------------------------------- /hotham-simulator/build_input/viewdisplay.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/hotham-simulator/build_input/viewdisplay.frag.spv -------------------------------------------------------------------------------- /hotham-simulator/build_input/viewdisplay.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout (location = 0) out vec2 outUV; 4 | 5 | out gl_PerVertex 6 | { 7 | vec4 gl_Position; 8 | }; 9 | 10 | void main() 11 | { 12 | outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); 13 | gl_Position = vec4(outUV * 2.0f - 1.0f, 0.0f, 1.0f); 14 | } -------------------------------------------------------------------------------- /hotham-simulator/build_input/viewdisplay.vert.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/hotham-simulator/build_input/viewdisplay.vert.spv -------------------------------------------------------------------------------- /hotham-simulator/build_input/wrapper.h: -------------------------------------------------------------------------------- 1 | #include "loader_interfaces.h" -------------------------------------------------------------------------------- /hotham-simulator/hotham_simulator.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_format_version": "1.0.0", 3 | "runtime": { 4 | "api_version": "1.0", 5 | "name": "Hotham Simulator", 6 | "library_path": "..\\target\\debug\\hotham_simulator.dll" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /hotham-simulator/openxr_simulator.reg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/hotham-simulator/openxr_simulator.reg -------------------------------------------------------------------------------- /hotham-simulator/src/action_state.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use openxr_sys::{Action, Path, FALSE}; 4 | 5 | #[derive(Debug, Clone, Default)] 6 | /// Stores Action state, allowing the simulator to simulate input for an application. 7 | // A bit yuck to use u64 instead of Action, but it doesn't support Hash.. but whatever. 8 | pub struct ActionState { 9 | boolean_actions: HashMap, 10 | bindings: HashMap, 11 | } 12 | impl ActionState { 13 | pub(crate) fn get_boolean(&self, action: Action) -> openxr_sys::Bool32 { 14 | self.boolean_actions 15 | .get(&action.into_raw()) 16 | .map(|p| (*p).into()) 17 | .unwrap_or(FALSE) 18 | } 19 | 20 | pub(crate) fn add_binding(&mut self, path: Path, action: Action) { 21 | self.bindings.insert(path, action.into_raw()); 22 | } 23 | 24 | /// Resets all action state. 25 | pub(crate) fn clear(&mut self) { 26 | // Set all the booleans to false. 27 | self.boolean_actions.values_mut().for_each(|v| *v = false); 28 | } 29 | 30 | pub(crate) fn set_boolean(&mut self, path: &Path, value: bool) { 31 | let action = self.bindings.get(path).unwrap(); 32 | self.boolean_actions.insert(*action, value); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /hotham-simulator/src/inputs.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use winit::event::{ElementState, KeyboardInput, VirtualKeyCode}; 4 | 5 | #[derive(Debug, Clone, Default)] 6 | pub struct Inputs { 7 | pub pressed: HashSet, 8 | } 9 | 10 | impl Inputs { 11 | pub fn process_event(&mut self, keyboard_input: KeyboardInput) { 12 | if let Some(key) = keyboard_input.virtual_keycode { 13 | let _ = match keyboard_input.state { 14 | ElementState::Pressed => self.pressed.insert(key), 15 | ElementState::Released => self.pressed.remove(&key), 16 | }; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /hotham-simulator/src/openxr_loader.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(deref_nullptr)] 5 | include!("./bindings.rs"); 6 | -------------------------------------------------------------------------------- /hotham-simulator/src/shaders/viewdisplay.frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/hotham-simulator/src/shaders/viewdisplay.frag.spv -------------------------------------------------------------------------------- /hotham-simulator/src/shaders/viewdisplay.vert.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/hotham-simulator/src/shaders/viewdisplay.vert.spv -------------------------------------------------------------------------------- /hotham-simulator/src/space_state.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | use openxr_sys::{Quaternionf, Vector3f}; 3 | 4 | #[derive(Clone)] 5 | pub struct SpaceState { 6 | pub name: String, 7 | pub position: Vector3f, 8 | pub orientation: Quaternionf, 9 | } 10 | 11 | impl SpaceState { 12 | pub fn new(name: &str) -> Self { 13 | Self { 14 | name: name.to_string(), 15 | position: Default::default(), 16 | orientation: Quaternionf::IDENTITY, 17 | } 18 | } 19 | } 20 | 21 | impl Debug for SpaceState { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | f.debug_struct("SpaceState") 24 | .field("name", &self.name) 25 | .field( 26 | "position", 27 | &format!( 28 | "x: {}, y: {}, z: {}", 29 | self.position.x, self.position.y, self.position.z 30 | ), 31 | ) 32 | .field( 33 | "orientation", 34 | &format!( 35 | "x: {}, y: {}, z: {}, w: {}", 36 | self.orientation.x, self.orientation.y, self.orientation.z, self.orientation.w 37 | ), 38 | ) 39 | .finish() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /hotham-simulator/test.ps1: -------------------------------------------------------------------------------- 1 | Write-Output "All your OpenXR are belong to crab" 2 | ${Env:RUST_BACKTRACE} = 3 3 | cargo build 4 | 5 | try { 6 | Push-Location -Path "C:\Users\kanem\Development\OpenXR-SDK-Source\build\src\tests\hello_xr\Debug" 7 | .\hello_xr.exe -g Vulkan2 8 | } 9 | catch { 10 | Write-Warning "Problem!" 11 | } 12 | finally { 13 | Pop-Location 14 | } -------------------------------------------------------------------------------- /hotham-simulator/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | echo "All your OpenXR are belong to crab" 4 | cargo build 5 | cd ../openxrs/openxr 6 | cargo run --example vulkan -------------------------------------------------------------------------------- /hotham/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | build = "build.rs" 3 | description = "A framework for creating incredible standalone VR experiences" 4 | edition = "2021" 5 | keywords = ["vr", "gamedev", "openxr", "vulkan"] 6 | license = "MIT OR Apache-2.0" 7 | name = "hotham" 8 | readme = "../README.md" 9 | repository = "https://github.com/leetvr/hotham/" 10 | version = "0.2.0" 11 | 12 | [dependencies] 13 | anyhow = "1.0" 14 | ash = "0.33.2" 15 | bitflags = "1.3" 16 | cpal = "0.15.2" 17 | ctrlc = {version = "3", features = ["termination"]} 18 | egui = "0.15" 19 | generational-arena = "0.2.8" 20 | glam = {features = ["mint", "serde", "approx"], version = "0.23"} 21 | gltf = {version = "1.0", features = ["KHR_lights_punctual", "KHR_materials_unlit", "names", "utils"], default-features = false} 22 | half = "2.1.0" 23 | hecs = "0.10.1" 24 | hotham-asset-client = {path = "../hotham-asset-client"} 25 | id-arena = "2.2.1" 26 | image = {version = "0.24.3", default-features = false, features = ["jpeg", "png"]} 27 | itertools = "0.10.0" 28 | # ktx2 = "0.3" 29 | ktx2 = {git = "https://github.com/BVE-Reborn/ktx2"} 30 | memoffset = "0.8.0" 31 | mint = "0.5.6" 32 | notify-debouncer-mini = "0.2.1" 33 | oddio = "0.5" 34 | openxr = {features = ["loaded", "mint"], version = "0.17"} 35 | rapier3d = "0.17" 36 | ruzstd = "0.3" 37 | serde = {version = "1.0", features = ["derive"]} 38 | symphonia = {version = "0.5", default-features = false, features = ["mp3"]} 39 | thiserror = "1.0" 40 | tokio = {version = "1.0.1", default-features = false, features = ["rt"]} 41 | vk-shader-macros = "0.2.8" 42 | 43 | [target.'cfg(not(any(target_os = "macos", target_os = "ios")))'.dev-dependencies] 44 | renderdoc = "0.10" 45 | 46 | [dev-dependencies] 47 | approx = "0.5" 48 | serde_json = "1.0" 49 | 50 | [target.'cfg(target_os = "android")'.dependencies] 51 | jni = "0.19.0" 52 | ndk = "0.6" 53 | ndk-glue = "0.6" 54 | 55 | [target.'cfg(target_os = "windows")'.dependencies] 56 | windows = {version = "0.48", features = ["Win32_Foundation", "Win32_System_Performance"]} 57 | 58 | [target.'cfg(not(target_os = "windows"))'.dependencies] 59 | libc = "0.2" 60 | -------------------------------------------------------------------------------- /hotham/build.rs: -------------------------------------------------------------------------------- 1 | pub fn main() { 2 | // On Android, we must ensure that we're dynamically linking against the C++ standard library. 3 | // For more details, see https://github.com/rust-windowing/android-ndk-rs/issues/167 4 | use std::env::var; 5 | if var("TARGET") 6 | .map(|target| target == "aarch64-linux-android") 7 | .unwrap_or(false) 8 | { 9 | println!("cargo:rustc-link-lib=dylib=c++"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /hotham/data/brdf_lut.ktx2: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:73fc7f8aa9f277891ed1c60d236e276057ab6f88795e3def8378ebb08af46764 3 | size 70600 4 | -------------------------------------------------------------------------------- /hotham/data/brdf_lut_android.ktx2: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:73fc7f8aa9f277891ed1c60d236e276057ab6f88795e3def8378ebb08af46764 3 | size 70600 4 | -------------------------------------------------------------------------------- /hotham/data/environment_map_diffuse.ktx2: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a39e0376673774c83dffe3363e73ee9fe788293649377ceaed2290357e8727d1 3 | size 196872 4 | -------------------------------------------------------------------------------- /hotham/data/environment_map_diffuse_android.ktx2: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0deee512ab4c020651d16627bb9aebe76b7eac2dd670fb16a2bfa1344b181cc3 3 | size 8816 4 | -------------------------------------------------------------------------------- /hotham/data/environment_map_specular.ktx2: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:33b1302bc3cb41d28f704df1395911ed5e96a639d7126fc2d9b0e76e1b0da6b9 3 | size 16777680 4 | -------------------------------------------------------------------------------- /hotham/data/environment_map_specular_android.ktx2: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8e08416a52eab7bcf43a2ac7894fce159aecbb2957a1e79f5464af5857a238ea 3 | size 524752 4 | -------------------------------------------------------------------------------- /hotham/src/asset_importer/scene.rs: -------------------------------------------------------------------------------- 1 | use crate::rendering::light::Light; 2 | 3 | use super::Models; 4 | 5 | /// Representation of a glTF Scene 6 | pub struct Scene { 7 | /// The models in the scene 8 | pub models: Models, 9 | /// The lights in the scene 10 | pub lights: Vec, 11 | } 12 | -------------------------------------------------------------------------------- /hotham/src/components/animation_controller.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use gltf::animation::util::ReadOutputs; 4 | use itertools::Itertools; 5 | 6 | use crate::{asset_importer::ImportContext, components::AnimationTarget}; 7 | use glam::Quat; 8 | 9 | #[derive(Debug, Clone, PartialEq, Default)] 10 | 11 | /// Component that controls how an `AnimationTarget` should be animated. 12 | /// Added by `gltf_loader` to the root node if its children contain animation data. 13 | pub struct AnimationController { 14 | /// The amount to blend from 15 | pub blend_from: usize, 16 | /// The amount to blend to 17 | pub blend_to: usize, 18 | /// The total blend amount 19 | pub blend_amount: f32, 20 | /// The targets to apply this animation to 21 | pub targets: Vec, 22 | } 23 | 24 | impl AnimationController { 25 | pub(crate) fn load( 26 | animations: gltf::iter::Animations, 27 | import_context: &mut ImportContext, 28 | ) -> AnimationController { 29 | let node_entity_map = &import_context.node_entity_map; 30 | let buffer = &import_context.buffer; 31 | 32 | let mut targets = HashMap::new(); 33 | 34 | for channel in animations.flat_map(|a| a.channels()) { 35 | let target = *node_entity_map 36 | .get(&channel.target().node().index()) 37 | .unwrap(); 38 | 39 | let animation_target = targets.entry(target).or_insert(AnimationTarget { 40 | target, 41 | rotations: Vec::new(), 42 | scales: Vec::new(), 43 | translations: Vec::new(), 44 | }); 45 | 46 | let reader = channel.reader(|_| Some(buffer)); 47 | match reader.read_outputs() { 48 | Some(ReadOutputs::Translations(translation_data)) => { 49 | for t in translation_data { 50 | animation_target 51 | .translations 52 | .push([t[0], t[1], t[2]].into()); 53 | } 54 | } 55 | Some(ReadOutputs::Rotations(rotation_data)) => { 56 | for r in rotation_data.into_f32() { 57 | animation_target 58 | .rotations 59 | .push(Quat::from_xyzw(r[0], r[1], r[2], r[3])); 60 | } 61 | } 62 | Some(ReadOutputs::Scales(scale_data)) => { 63 | for s in scale_data { 64 | animation_target.scales.push([s[0], s[1], s[2]].into()); 65 | } 66 | } 67 | _ => {} 68 | } 69 | } 70 | 71 | AnimationController { 72 | blend_from: 0, 73 | blend_to: 1, 74 | blend_amount: 0., 75 | targets: targets.drain().map(|n| n.1).collect_vec(), 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /hotham/src/components/animation_target.rs: -------------------------------------------------------------------------------- 1 | use glam::{Quat, Vec3}; 2 | use hecs::Entity; 3 | 4 | /// A component that allows an entity to be animated. 5 | /// Usually added by `gltf_loader` if the node contains animation data. 6 | #[derive(Debug, Clone, PartialEq)] 7 | pub struct AnimationTarget { 8 | /// The entity that is affected by this animation 9 | pub target: Entity, 10 | /// Rotations for this animation 11 | pub rotations: Vec, 12 | /// Scales for this animation 13 | pub scales: Vec, 14 | /// Translations for this animation 15 | pub translations: Vec, 16 | } 17 | -------------------------------------------------------------------------------- /hotham/src/components/global_transform.rs: -------------------------------------------------------------------------------- 1 | use crate::util; 2 | use glam::{Affine3A, Quat, Vec3}; 3 | 4 | use super::LocalTransform; 5 | 6 | /// Component used to represent the global transform of the entity in the renderer. 7 | /// This is the transformation from local to global space. 8 | #[derive(Debug, Clone, Copy, PartialEq)] 9 | pub struct GlobalTransform(pub Affine3A); 10 | 11 | impl Default for GlobalTransform { 12 | fn default() -> Self { 13 | Self(Affine3A::IDENTITY) 14 | } 15 | } 16 | 17 | impl GlobalTransform { 18 | /// Convenience function to convert the [`GlobalTransform`] into a [`rapier3d::na::Isometry3`] 19 | pub fn to_isometry(&self) -> rapier3d::na::Isometry3 { 20 | util::isometry_from_affine(&self.0) 21 | } 22 | 23 | /// Convenience function to decompose the [`GlobalTransform`] into its components 24 | pub fn to_scale_rotation_translation(&self) -> (Vec3, Quat, Vec3) { 25 | self.0.to_scale_rotation_translation() 26 | } 27 | } 28 | 29 | impl From for GlobalTransform { 30 | fn from(l: LocalTransform) -> Self { 31 | GlobalTransform(l.to_affine()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /hotham/src/components/grabbable.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy)] 2 | pub struct Grabbable {} 3 | 4 | #[derive(Debug, Clone, Copy)] 5 | pub struct Grabbed; 6 | 7 | #[derive(Debug, Clone, Copy)] 8 | pub struct Released; 9 | -------------------------------------------------------------------------------- /hotham/src/components/hand.rs: -------------------------------------------------------------------------------- 1 | use glam::Affine3A; 2 | use hecs::Entity; 3 | 4 | /// A component that represents the "side" or "handedness" that an entity is on 5 | /// Used by components such as `Hand` and `Pointer` to identify which controller they should map to 6 | #[derive(Debug, PartialEq, Clone, Copy, Eq, PartialOrd, Ord)] 7 | pub enum Handedness { 8 | /// Left hand side 9 | Left, 10 | /// Right hand side 11 | Right, 12 | } 13 | 14 | #[derive(Clone)] 15 | pub struct GrabbedEntity { 16 | pub entity: Entity, 17 | pub grip_from_local: Affine3A, 18 | } 19 | 20 | /// A component that's added to an entity to represent a "hand" presence. 21 | /// Used to give the player a feeling of immersion by allowing them to grab objects in the world 22 | /// Requires `hands_system` 23 | #[derive(Clone)] 24 | pub struct Hand { 25 | /// How much has this hand been gripped? 26 | pub grip_value: f32, 27 | /// Did the grip button go from not pressed to pressed this frame? 28 | pub grip_button_just_pressed: bool, 29 | /// Which side is this hand on? 30 | pub handedness: Handedness, 31 | /// Have we grabbed something? 32 | pub grabbed_entity: Option, 33 | } 34 | 35 | impl Hand { 36 | /// Shortcut helper to create a Left hand 37 | pub fn left() -> Hand { 38 | Hand { 39 | grip_value: 0.0, 40 | grip_button_just_pressed: false, 41 | handedness: Handedness::Left, 42 | grabbed_entity: None, 43 | } 44 | } 45 | 46 | /// Shortcut helper to create a right hand 47 | pub fn right() -> Hand { 48 | Hand { 49 | grip_value: 0.0, 50 | grip_button_just_pressed: false, 51 | handedness: Handedness::Right, 52 | grabbed_entity: None, 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /hotham/src/components/hmd.rs: -------------------------------------------------------------------------------- 1 | /// A marker component used to indicate the player's headset, or Head Mounted Display in the game simulation. 2 | /// 3 | /// The entity marked with this component will have its [`super::LocalTransform`] is updated each frame by the 4 | /// engine with the pose of the player's headset in the real world (ie. stage space). Since this entity is 5 | /// parented to the [`super::Stage`] entity, querying for the HMD's [`super::GlobalTransform`] will then give 6 | /// the pose of the HMD in the virtual world (ie. global space). 7 | /// 8 | /// This is very important when incorporating the user's position in the real world into the game simulation, 9 | /// ie. player controllers. Future versions of Hotham may add more functionality to make this even easier. 10 | pub struct HMD {} 11 | -------------------------------------------------------------------------------- /hotham/src/components/info.rs: -------------------------------------------------------------------------------- 1 | /// Component that adds some information about the entity 2 | /// Useful for debugging - added by default by `gltf_loader` 3 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] 4 | pub struct Info { 5 | /// A helpful name 6 | pub name: String, 7 | /// Node ID from the original glTF file 8 | pub node_id: usize, 9 | } 10 | -------------------------------------------------------------------------------- /hotham/src/components/joint.rs: -------------------------------------------------------------------------------- 1 | use glam::Affine3A; 2 | use hecs::Entity; 3 | 4 | /// A component that adds a "skinned joint" to an entity. 5 | /// For more detail, check out the [glTF spec](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#skins-overview) 6 | /// Automatically added by `gltf_loader` for nodes that contain skin data 7 | #[derive(Debug, Clone, Copy, PartialEq)] 8 | pub struct Joint { 9 | /// Pointer to the root of the skeleton 10 | pub skeleton_root: Entity, 11 | /// Inverse bind matrix used to apply the skin in the vertex shader 12 | pub inverse_bind_matrix: Affine3A, 13 | } 14 | -------------------------------------------------------------------------------- /hotham/src/components/local_transform.rs: -------------------------------------------------------------------------------- 1 | use glam::{Affine3A, Quat, Vec3}; 2 | use gltf::scene::Transform as TransformData; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::util::{decompose_isometry, isometry_from_affine}; 6 | 7 | /// The component's position in global space (ie. the game simulation), relative to its parent. 8 | /// 9 | /// There are two ways to set an entity's position in Hotham: 10 | /// 11 | /// 1. **Game controlled** - this entity will have its rigid body position set by the **game** simulation 12 | /// 1. **Physics controlled** - this entity will have its position set by the **physics** simulation 13 | /// 14 | /// If an entity has a [`super::RigidBody`] component with a `body_type` of [`super::physics::BodyType::Dynamic`], then you 15 | /// are indicating that you want this entity's position in the game simulation (ie. its global position) 16 | /// to be entirely controlled by the physics simulation. 17 | /// 18 | /// Otherwise, you can just modify [`LocalTransform`] and your entity will have its position in the game 19 | /// simulation AND the physics simulation (if it has a [`super::RigidBody`] and/or [`super::Collider`]) set relative to its [`super::Parent`]. 20 | /// 21 | /// If the entity doesn't have a [`super::Parent`], then the [`super::GlobalTransform`] is just whatever you've set here. 22 | #[derive(Clone, PartialEq, Debug, Copy, Deserialize, Serialize)] 23 | pub struct LocalTransform { 24 | /// The translation of the entity 25 | pub translation: Vec3, 26 | /// The rotation of the entity 27 | pub rotation: Quat, 28 | /// The non-uniform scale of the entity 29 | pub scale: Vec3, 30 | } 31 | 32 | impl Default for LocalTransform { 33 | fn default() -> Self { 34 | Self { 35 | translation: Vec3::ZERO, 36 | rotation: Quat::IDENTITY, 37 | scale: Vec3::ONE, 38 | } 39 | } 40 | } 41 | 42 | impl LocalTransform { 43 | pub(crate) fn load(transform_data: TransformData) -> LocalTransform { 44 | let (t, r, s) = transform_data.decomposed(); 45 | let translation = t.into(); 46 | let rotation = Quat::from_xyzw(r[0], r[1], r[2], r[3]); 47 | let scale = s.into(); 48 | 49 | LocalTransform { 50 | scale, 51 | rotation, 52 | translation, 53 | } 54 | } 55 | 56 | pub fn from_rotation_translation(rotation: Quat, translation: Vec3) -> Self { 57 | LocalTransform { 58 | rotation, 59 | translation, 60 | ..Default::default() 61 | } 62 | } 63 | 64 | /// Convenience function to convert the [`LocalTransform`] into a [`rapier3d::na::Isometry3`] 65 | pub fn to_isometry(&self) -> rapier3d::na::Isometry3 { 66 | isometry_from_affine(&self.to_affine()) 67 | } 68 | 69 | /// Update the translation and rotation from a [`rapier3d::na::Isometry3`] 70 | pub fn update_from_isometry(&mut self, isometry: &rapier3d::na::Isometry3) { 71 | (self.rotation, self.translation) = decompose_isometry(isometry); 72 | } 73 | 74 | /// Update the scale, rotation and rotation from a [`glam::Affine3A`] 75 | pub fn update_from_affine(&mut self, transform: &glam::Affine3A) { 76 | (self.scale, self.rotation, self.translation) = transform.to_scale_rotation_translation(); 77 | } 78 | 79 | /// Update ONLY rotation and rotation from a [`glam::Affine3A`] 80 | pub fn update_rotation_translation_from_affine(&mut self, transform: &glam::Affine3A) { 81 | (_, self.rotation, self.translation) = transform.to_scale_rotation_translation(); 82 | } 83 | 84 | /// Convenience function to convert the [`LocalTransform`] into a [`glam::Affine3A`] 85 | pub fn to_affine(&self) -> Affine3A { 86 | Affine3A::from_scale_rotation_translation(self.scale, self.rotation, self.translation) 87 | } 88 | } 89 | 90 | impl From for Affine3A { 91 | fn from(l: LocalTransform) -> Self { 92 | l.to_affine() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /hotham/src/components/mesh.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | asset_importer::ImportContext, contexts::RenderContext, id_arena::Id, 3 | rendering::mesh_data::MeshData, 4 | }; 5 | 6 | /// A handle to MeshData stored on the GPU. 7 | #[derive(Debug, Clone)] 8 | pub struct Mesh { 9 | pub handle: Id, 10 | } 11 | 12 | impl Mesh { 13 | /// Create a new mesh handle from mesh data. 14 | pub fn new(mesh_data: MeshData, render_context: &mut RenderContext) -> Self { 15 | let handle = render_context.resources.mesh_data.alloc(mesh_data); 16 | Mesh { handle } 17 | } 18 | 19 | /// Takes mesh data from a glTF file, uploads it to the GPU and inserts it into mesh_map 20 | pub(crate) fn load(gltf_mesh_data: gltf::Mesh, import_context: &mut ImportContext) { 21 | let index = gltf_mesh_data.index(); 22 | let mesh_data = MeshData::load(gltf_mesh_data, import_context); 23 | 24 | import_context 25 | .mesh_map 26 | .insert(index, Mesh::new(mesh_data, import_context.render_context)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /hotham/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | pub mod animation_controller; 3 | pub mod animation_target; 4 | pub mod global_transform; 5 | pub mod grabbable; 6 | pub mod hand; 7 | pub mod hmd; 8 | pub mod info; 9 | pub mod joint; 10 | pub mod local_transform; 11 | pub mod mesh; 12 | pub mod panel; 13 | pub mod parent; 14 | pub mod physics; 15 | pub mod pointer; 16 | pub mod root; 17 | pub mod skin; 18 | pub mod sound_emitter; 19 | pub mod stage; 20 | pub mod ui_panel; 21 | pub mod visible; 22 | 23 | pub use animation_controller::AnimationController; 24 | pub use animation_target::AnimationTarget; 25 | pub use global_transform::GlobalTransform; 26 | pub use grabbable::*; 27 | pub use hand::Hand; 28 | pub use hmd::HMD; 29 | pub use info::Info; 30 | pub use joint::Joint; 31 | pub use local_transform::LocalTransform; 32 | pub use mesh::Mesh; 33 | pub use panel::Panel; 34 | pub use parent::Parent; 35 | pub use physics::collider::Collider; 36 | pub use physics::RigidBody; 37 | pub use pointer::Pointer; 38 | pub use root::Root; 39 | pub use skin::Skin; 40 | pub use sound_emitter::SoundEmitter; 41 | pub use stage::Stage; 42 | pub use ui_panel::UIPanel; 43 | pub use visible::Visible; 44 | -------------------------------------------------------------------------------- /hotham/src/components/panel.rs: -------------------------------------------------------------------------------- 1 | use ash::vk::{self}; 2 | use egui::Pos2; 3 | use glam::Vec2; 4 | 5 | use crate::components::Mesh; 6 | use crate::hotham_error::HothamError; 7 | use crate::rendering::material::{pack2x16, Material, MaterialFlags}; 8 | use crate::rendering::mesh_data::MeshData; 9 | use crate::rendering::primitive::Primitive; 10 | use crate::rendering::vertex::Vertex; 11 | use crate::{ 12 | contexts::{RenderContext, VulkanContext}, 13 | rendering::texture::Texture, 14 | }; 15 | 16 | pub struct Panel { 17 | /// The resolution of the Panel 18 | pub resolution: vk::Extent2D, 19 | /// The size of the panel in the world 20 | pub world_size: Vec2, 21 | /// Texture backing the Panel 22 | pub texture: Texture, 23 | /// Input received this frame 24 | pub input: Option, 25 | } 26 | 27 | impl Panel { 28 | pub fn create( 29 | vulkan_context: &VulkanContext, 30 | render_context: &mut RenderContext, 31 | resolution: vk::Extent2D, 32 | world_size: Vec2, 33 | ) -> Result<(Panel, Mesh), HothamError> { 34 | let texture = Texture::empty(vulkan_context, render_context, resolution); 35 | let mesh = create_panel_mesh(&texture, render_context, world_size); 36 | 37 | Ok(( 38 | Panel { 39 | resolution, 40 | world_size, 41 | texture, 42 | input: Default::default(), 43 | }, 44 | mesh, 45 | )) 46 | } 47 | } 48 | 49 | fn create_panel_mesh( 50 | output_texture: &Texture, 51 | render_context: &mut RenderContext, 52 | world_size: Vec2, 53 | ) -> Mesh { 54 | let material_id = add_material(output_texture, render_context); 55 | let (half_width, half_height) = (world_size.x / 2., world_size.y / 2.); 56 | 57 | let positions = [ 58 | [-half_width, half_height, 0.].into(), // v0 59 | [half_width, -half_height, 0.].into(), // v1 60 | [half_width, half_height, 0.].into(), // v2 61 | [-half_width, -half_height, 0.].into(), // v3 62 | ]; 63 | let tex_coords_0: [glam::Vec2; 4] = [ 64 | [0., 0.].into(), // v0 65 | [1., 1.].into(), // v1 66 | [1., 0.].into(), // v2 67 | [0., 1.].into(), // v3 68 | ]; 69 | let vertices: Vec = tex_coords_0 70 | .iter() 71 | .map(|t| Vertex { 72 | texture_coords: *t, 73 | ..Default::default() 74 | }) 75 | .collect(); 76 | 77 | let indices = [0, 1, 2, 0, 3, 1]; 78 | let primitive = Primitive::new(&positions, &vertices, &indices, material_id, render_context); 79 | Mesh::new(MeshData::new(vec![primitive]), render_context) 80 | } 81 | 82 | fn add_material(output_texture: &Texture, render_context: &mut RenderContext) -> u32 { 83 | let mut material = Material::unlit_white(); 84 | material.packed_flags_and_base_texture_id = pack2x16( 85 | (MaterialFlags::HAS_BASE_COLOR_TEXTURE | MaterialFlags::UNLIT_WORKFLOW).bits(), 86 | output_texture.index, 87 | ); 88 | unsafe { render_context.resources.materials_buffer.push(&material) } 89 | } 90 | 91 | /// Input to a panel 92 | #[derive(Debug, Clone)] 93 | pub struct PanelInput { 94 | /// Location of the cursor, in panel space 95 | pub cursor_location: Pos2, 96 | /// Value of the controller trigger 97 | pub trigger_value: f32, 98 | } 99 | -------------------------------------------------------------------------------- /hotham/src/components/parent.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | 3 | /// Component added to indicate that an entity has a parent 4 | /// Used by `update_global_transform_with_parent_system` 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 6 | pub struct Parent(pub Entity); 7 | -------------------------------------------------------------------------------- /hotham/src/components/physics/additional_mass.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Default)] 2 | pub struct AdditionalMass { 3 | pub value: f32, 4 | } 5 | 6 | impl AdditionalMass { 7 | pub fn new(value: f32) -> Self { 8 | Self { value } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /hotham/src/components/physics/collider.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use hecs::Entity; 4 | pub use rapier3d::prelude::{ActiveCollisionTypes, Group, SharedShape}; 5 | 6 | use crate::contexts::physics_context::{DEFAULT_COLLISION_GROUP, HAND_COLLISION_GROUP}; 7 | 8 | /// A component that enables collision detection - essentially a thin wrapper around [`rapier3d::prelude::Collider`]. 9 | #[derive(Clone)] 10 | pub struct Collider { 11 | /// A list of entities that may have collided with this one this frame 12 | pub collisions_this_frame: Vec, 13 | /// The shape of this collider 14 | pub shape: SharedShape, 15 | /// Is this a sensor collider? 16 | pub sensor: bool, 17 | /// What collision groups is this a member of? 18 | pub collision_groups: Group, 19 | /// What groups can this collider interact with? 20 | pub collision_filter: Group, 21 | /// What kinds of colliders can this collider interact with? 22 | pub active_collision_types: ActiveCollisionTypes, 23 | /// Should this collider be offset from its parent (if it has one)? 24 | pub offset_from_parent: glam::Vec3, 25 | /// How "bouncy" is this collider? 26 | pub restitution: f32, 27 | /// What is the mass of this collider? 28 | pub mass: f32, 29 | } 30 | 31 | impl Debug for Collider { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | f.debug_struct("Collider") 34 | .field("collisions_this_frame", &self.collisions_this_frame) 35 | .field("shape", &self.shape.shape_type()) 36 | .field("sensor", &self.sensor) 37 | .field("collision_groups", &self.collision_groups) 38 | .field("collision_filter", &self.collision_filter) 39 | .field("active_collision_types", &self.active_collision_types) 40 | .field("offset_from_parent", &self.offset_from_parent) 41 | .field("restitution", &self.restitution) 42 | .field("mass", &self.mass) 43 | .finish() 44 | } 45 | } 46 | 47 | impl Collider { 48 | /// Create a new collider 49 | pub fn new(shape: SharedShape) -> Collider { 50 | Collider { 51 | shape, 52 | ..Default::default() 53 | } 54 | } 55 | } 56 | 57 | impl Default for Collider { 58 | fn default() -> Self { 59 | Self { 60 | collisions_this_frame: Default::default(), 61 | shape: SharedShape::ball(1.0), 62 | sensor: false, 63 | collision_groups: DEFAULT_COLLISION_GROUP, 64 | collision_filter: DEFAULT_COLLISION_GROUP | HAND_COLLISION_GROUP, 65 | active_collision_types: ActiveCollisionTypes::default(), 66 | offset_from_parent: Default::default(), 67 | restitution: 0., 68 | mass: 0., 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /hotham/src/components/physics/impulse.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct Impulse { 3 | pub value: glam::Vec3, 4 | } 5 | 6 | impl Impulse { 7 | pub fn new(value: glam::Vec3) -> Self { 8 | Self { value } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /hotham/src/components/physics/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod additional_mass; 2 | pub mod collider; 3 | pub mod impulse; 4 | pub mod rigid_body; 5 | pub mod teleport; 6 | 7 | pub use additional_mass::AdditionalMass; 8 | pub use collider::ActiveCollisionTypes; 9 | pub use collider::Collider; 10 | pub use collider::SharedShape; 11 | pub use impulse::Impulse; 12 | pub use rigid_body::BodyType; 13 | pub use rigid_body::RigidBody; 14 | pub use teleport::Teleport; 15 | -------------------------------------------------------------------------------- /hotham/src/components/physics/rigid_body.rs: -------------------------------------------------------------------------------- 1 | use rapier3d::prelude::RigidBodyType as RapierBodyType; 2 | 3 | /// A component used to synchronise this entity's position in the game simulation with the physics simulation. 4 | /// 5 | /// You can indicate to [`crate::systems::physics_system`] how you'd like this entity to be treated by changing the `body_type` field 6 | /// . Setting the `body_type` to [`BodyType::Dynamic`] will result in the entity having its [`crate::components::GlobalTransform`] 7 | /// overwritten by its position in the physics simulation - any updates to [`crate::components::LocalTransform`] or [`crate::components::GlobalTransform`] will be overwritten. 8 | /// 9 | /// Any other kind of body is treated as *game controlled* - that is, updating the entity's [`crate::components::LocalTransform`] will not be overwritten 10 | /// and the position of the entity in the physics simulation will be updated based on its [`crate::components::GlobalTransform`] (all transforms in the 11 | /// physics simulation are in global space). 12 | /// 13 | /// ## Panics 14 | /// 15 | /// Trying to create a [`RigidBody`] with a `body_type` of [`BodyType::Dynamic`], or change an existing [`RigidBody`]'s `body_type` to 16 | /// be [`BodyType::Dynamic`] on a [`hecs::Entity`] that has a [`Parent`] component will cause a panic. Don't do it. 17 | #[derive(Debug, Clone)] 18 | pub struct RigidBody { 19 | pub body_type: BodyType, 20 | pub linear_velocity: glam::Vec3, 21 | pub angular_velocity: glam::Vec3, 22 | pub mass: f32, 23 | pub lock_rotations: bool, 24 | } 25 | 26 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 27 | pub enum BodyType { 28 | KinematicPositionBased, 29 | KinematicVelocityBased, 30 | Dynamic, 31 | Fixed, 32 | } 33 | 34 | impl From for RapierBodyType { 35 | fn from(r: BodyType) -> Self { 36 | match r { 37 | BodyType::KinematicPositionBased => RapierBodyType::KinematicPositionBased, 38 | BodyType::KinematicVelocityBased => RapierBodyType::KinematicVelocityBased, 39 | BodyType::Dynamic => RapierBodyType::Dynamic, 40 | BodyType::Fixed => RapierBodyType::Fixed, 41 | } 42 | } 43 | } 44 | 45 | impl Default for RigidBody { 46 | fn default() -> Self { 47 | Self { 48 | body_type: BodyType::Dynamic, 49 | linear_velocity: Default::default(), 50 | angular_velocity: Default::default(), 51 | mass: 0., 52 | lock_rotations: false, 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /hotham/src/components/physics/teleport.rs: -------------------------------------------------------------------------------- 1 | /// A "one-shot" tag component that indicates to the physics system that you would like 2 | /// this entity to be teleported - ie. moved in a non-physical way. The mechanism for this is simple: 3 | /// 4 | /// 1. Insert this component to the entity you'd like to teleport 5 | /// 2. Set the entity's LocalTransform to where you'd like the entity teleported 6 | /// 7 | /// In the next tick, this component will be removed. 8 | pub struct Teleport {} 9 | -------------------------------------------------------------------------------- /hotham/src/components/pointer.rs: -------------------------------------------------------------------------------- 1 | use super::hand::Handedness; 2 | 3 | /// A component added to an entity to allow users to interact with `UIPanels` using their 4 | /// controllers. 5 | pub struct Pointer { 6 | /// Which hand is the pointer in? 7 | pub handedness: Handedness, 8 | /// How much has the trigger been pulled down? 9 | pub trigger_value: f32, 10 | } 11 | -------------------------------------------------------------------------------- /hotham/src/components/root.rs: -------------------------------------------------------------------------------- 1 | /// Component to represent that this is the root entity of a glTF model 2 | /// Automatically added by `gltf_loader` 3 | #[derive(Debug, Clone, Copy, Default)] 4 | pub struct Root {} 5 | -------------------------------------------------------------------------------- /hotham/src/components/skin.rs: -------------------------------------------------------------------------------- 1 | use crate::{asset_importer::ImportContext, rendering::resources::MAX_JOINTS}; 2 | use glam::{Affine3A, Mat4}; 3 | use hecs::Entity; 4 | 5 | pub static NO_SKIN: u32 = std::u32::MAX; 6 | 7 | /// Component added to an entity to point to the joints in the node 8 | /// Automatically added by `gltf_loader` 9 | #[derive(Debug, Clone, PartialEq)] 10 | pub struct Skin { 11 | /// List of joints 12 | pub joints: Vec, 13 | /// Inverse bind matrices, used to build the final joint matrices for this skin 14 | pub inverse_bind_matrices: Vec, 15 | /// Index into skin buffer 16 | pub id: u32, 17 | } 18 | 19 | impl Skin { 20 | pub(crate) fn load(skin: gltf::Skin, import_context: &mut ImportContext) -> Skin { 21 | let reader = skin.reader(|_| Some(&import_context.buffer)); 22 | let inverse_bind_matrices = reader 23 | .read_inverse_bind_matrices() 24 | .unwrap() 25 | .map(|m| Affine3A::from_mat4(Mat4::from_cols_array_2d(&m))) 26 | .collect(); 27 | 28 | let joints = skin 29 | .joints() 30 | .map(|j| { 31 | import_context 32 | .node_entity_map 33 | .get(&j.index()) 34 | .cloned() 35 | .unwrap() 36 | }) 37 | .collect(); 38 | 39 | let empty_matrices = [Mat4::IDENTITY; MAX_JOINTS]; 40 | let id = unsafe { 41 | import_context 42 | .render_context 43 | .resources 44 | .skins_buffer 45 | .push(&empty_matrices) 46 | }; 47 | 48 | Skin { 49 | joints, 50 | id, 51 | inverse_bind_matrices, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hotham/src/components/sound_emitter.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use oddio::{Frames, Stop}; 4 | 5 | type AudioHandle = oddio::Handle>>>; 6 | 7 | /// A component added to an entity to allow it to emit a sound, usually a sound effect 8 | /// Used by `audio_system` 9 | pub struct SoundEmitter { 10 | /// The actual sound data 11 | pub frames: Arc>, 12 | /// Handle into the `oddio` spatializer 13 | pub handle: Option, 14 | /// Used to indicate that the emitter wants to change its state 15 | pub next_state: Option, 16 | } 17 | 18 | impl Clone for SoundEmitter { 19 | fn clone(&self) -> Self { 20 | Self { 21 | frames: self.frames.clone(), 22 | handle: None, 23 | next_state: None, 24 | } 25 | } 26 | } 27 | 28 | /// State of a sound 29 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 30 | pub enum SoundState { 31 | /// The sound has stopped permanently 32 | Stopped, 33 | /// The sound is playing 34 | Playing, 35 | /// The sound is temporarily paused 36 | Paused, 37 | } 38 | 39 | impl SoundEmitter { 40 | /// Convenience function to create a new `SoundEmitter` 41 | pub fn new(frames: Arc>) -> Self { 42 | Self { 43 | frames, 44 | handle: None, 45 | next_state: None, 46 | } 47 | } 48 | 49 | /// Convenience function to get the `SoundState` of this `SoundEmitter` 50 | pub fn current_state(&mut self) -> SoundState { 51 | if let Some(handle) = self.handle.as_mut() { 52 | let control = handle.control::, _>(); 53 | if control.is_paused() { 54 | return SoundState::Paused; 55 | } 56 | if control.is_stopped() { 57 | return SoundState::Stopped; 58 | } 59 | SoundState::Playing 60 | } else { 61 | SoundState::Stopped 62 | } 63 | } 64 | 65 | /// Play the sound 66 | pub fn play(&mut self) { 67 | self.next_state = Some(SoundState::Playing); 68 | } 69 | 70 | /// Pause the sound 71 | pub fn pause(&mut self) { 72 | self.next_state = Some(SoundState::Paused); 73 | } 74 | 75 | /// Stop the sound 76 | pub fn stop(&mut self) { 77 | self.next_state = Some(SoundState::Stopped); 78 | } 79 | 80 | /// Resume the sound 81 | pub fn resume(&mut self) { 82 | self.next_state = Some(SoundState::Playing); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /hotham/src/components/stage.rs: -------------------------------------------------------------------------------- 1 | /// A component to assist with artificial locomotion. 2 | /// 3 | /// On startup, [`crate::Engine`] will create an entity with the [`Stage`] component, and an entity with the 4 | /// [`super::HMD`] component (representing the user's headset), parented to the stage entity. 5 | /// 6 | /// This makes moving the player around in the virtual world *relatively* straightforward, as all is required is 7 | /// to move the [`Stage`]. The engine will ensure that the player's position in the game simulation AND the 8 | /// virtual cameras are correctly updated to account for this. 9 | /// 10 | /// In short, the final position of the player in the game simulation (ie. global space) is: 11 | /// 12 | /// `stage.position * hmd.position` 13 | /// 14 | /// *You* are responsible for controlling the [`Stage`], and the *engine* will update the [`super::HMD`]. 15 | /// 16 | /// For more information on how this works, check out [`super::HMD`] and [`crate::contexts::InputContext`]. 17 | #[derive(Debug)] 18 | pub struct Stage; 19 | 20 | use glam::Affine3A; 21 | use hecs::With; 22 | 23 | use crate::{components::GlobalTransform, hecs::World}; 24 | 25 | /// Get the transform of the stage in global space. 26 | pub fn get_global_from_stage(world: &World) -> Affine3A { 27 | // Get the stage transform 28 | world 29 | .query::>() 30 | .into_iter() 31 | .next() 32 | .map(|(_, global_transform)| global_transform.0) 33 | .unwrap_or(Affine3A::IDENTITY) 34 | } 35 | -------------------------------------------------------------------------------- /hotham/src/components/visible.rs: -------------------------------------------------------------------------------- 1 | /// The Visibility component determines whether a given entity is shown or hidden within the world. 2 | /// 3 | /// During each tick of the Hotham engine, entities can have Visibility assigned or removed. 4 | /// 5 | /// Basic usage: 6 | /// ```ignore 7 | /// use hotham::components::Visible; 8 | /// world.insert_one(entity, Visible {}); 9 | /// world.remove_one::(entity); 10 | /// ``` 11 | 12 | #[derive(Debug, Clone, Copy)] 13 | pub struct Visible {} 14 | -------------------------------------------------------------------------------- /hotham/src/contexts/haptic_context.rs: -------------------------------------------------------------------------------- 1 | use crate::components::hand::Handedness; 2 | 3 | /// Wrapper around XR Haptics 4 | #[derive(Clone, Debug, Default)] 5 | pub struct HapticContext { 6 | /// Haptics that should be applied to the left hand 7 | pub left_hand_amplitude_this_frame: f32, 8 | /// Haptics that should be applied to the right hand 9 | pub right_hand_amplitude_this_frame: f32, 10 | } 11 | 12 | impl HapticContext { 13 | /// Request haptics be applied this frame 14 | pub fn request_haptic_feedback(&mut self, amplitude: f32, handedness: Handedness) { 15 | match handedness { 16 | Handedness::Left => { 17 | if amplitude > self.left_hand_amplitude_this_frame { 18 | self.left_hand_amplitude_this_frame = amplitude; 19 | } 20 | } 21 | Handedness::Right => { 22 | if amplitude > self.right_hand_amplitude_this_frame { 23 | self.right_hand_amplitude_this_frame = amplitude; 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /hotham/src/contexts/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | pub mod audio_context; 3 | pub mod gui_context; 4 | pub mod haptic_context; 5 | pub mod input_context; 6 | pub mod physics_context; 7 | pub mod render_context; 8 | pub mod vulkan_context; 9 | pub mod xr_context; 10 | 11 | pub use audio_context::AudioContext; 12 | pub use gui_context::GuiContext; 13 | pub use haptic_context::HapticContext; 14 | pub use input_context::InputContext; 15 | pub use physics_context::PhysicsContext; 16 | pub use render_context::RenderContext; 17 | pub use vulkan_context::VulkanContext; 18 | pub use xr_context::{XrContext, XrContextBuilder}; 19 | -------------------------------------------------------------------------------- /hotham/src/contexts/physics_context.rs: -------------------------------------------------------------------------------- 1 | use rapier3d::{ 2 | crossbeam::{self, channel::Receiver}, 3 | na::Matrix3x1, 4 | prelude::*, 5 | }; 6 | 7 | pub const DEFAULT_COLLISION_GROUP: Group = Group::GROUP_1; 8 | pub const PANEL_COLLISION_GROUP: Group = Group::GROUP_2; 9 | pub const HAND_COLLISION_GROUP: Group = Group::GROUP_3; 10 | pub const WALL_COLLISION_GROUP: Group = Group::GROUP_4; 11 | pub const SENSOR_COLLISION_GROUP: Group = Group::GROUP_5; 12 | 13 | /// TODO: This is *usually* 72fps on the Quest 2, but we may support higher resolutions later. 14 | pub const DELTA_TIME: f32 = 1. / 72.; 15 | 16 | pub struct PhysicsContext { 17 | pub physics_pipeline: PhysicsPipeline, 18 | pub gravity: Matrix3x1, 19 | pub query_pipeline: QueryPipeline, 20 | pub colliders: ColliderSet, 21 | pub broad_phase: BroadPhase, 22 | pub narrow_phase: NarrowPhase, 23 | pub rigid_bodies: RigidBodySet, 24 | pub island_manager: IslandManager, 25 | pub collision_recv: Receiver, 26 | pub contact_force_recv: Receiver, 27 | pub event_handler: ChannelEventCollector, 28 | pub integration_parameters: IntegrationParameters, 29 | pub impulse_joints: ImpulseJointSet, 30 | pub multibody_joints: MultibodyJointSet, 31 | pub ccd_solver: CCDSolver, 32 | } 33 | 34 | impl Default for PhysicsContext { 35 | fn default() -> Self { 36 | let (collision_send, collision_recv) = crossbeam::channel::unbounded(); 37 | let (contact_force_send, contact_force_recv) = crossbeam::channel::unbounded(); 38 | let event_handler = ChannelEventCollector::new(collision_send, contact_force_send); 39 | let integration_parameters = IntegrationParameters { 40 | dt: DELTA_TIME, 41 | ..Default::default() 42 | }; 43 | 44 | let physics_pipeline = PhysicsPipeline::new(); 45 | let impulse_joints = ImpulseJointSet::new(); 46 | let multibody_joints = MultibodyJointSet::new(); 47 | let ccd_solver = CCDSolver::new(); 48 | 49 | PhysicsContext { 50 | physics_pipeline, 51 | gravity: [0., 0., 0.].into(), 52 | query_pipeline: QueryPipeline::new(), 53 | colliders: ColliderSet::new(), 54 | broad_phase: BroadPhase::new(), 55 | narrow_phase: NarrowPhase::new(), 56 | rigid_bodies: RigidBodySet::new(), 57 | island_manager: IslandManager::new(), 58 | collision_recv, 59 | contact_force_recv, 60 | event_handler, 61 | integration_parameters, 62 | impulse_joints, 63 | multibody_joints, 64 | ccd_solver, 65 | } 66 | } 67 | } 68 | 69 | impl PhysicsContext { 70 | pub fn update(&mut self) { 71 | self.physics_pipeline.step( 72 | &self.gravity, 73 | &self.integration_parameters, 74 | &mut self.island_manager, 75 | &mut self.broad_phase, 76 | &mut self.narrow_phase, 77 | &mut self.rigid_bodies, 78 | &mut self.colliders, 79 | &mut self.impulse_joints, 80 | &mut self.multibody_joints, 81 | &mut self.ccd_solver, 82 | Some(&mut self.query_pipeline), 83 | &(), 84 | &(), 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /hotham/src/contexts/xr_context/time.rs: -------------------------------------------------------------------------------- 1 | use super::XrContext; 2 | use crate::HothamResult; 3 | 4 | #[cfg(target_os = "windows")] 5 | impl XrContext { 6 | pub fn now(&self) -> HothamResult { 7 | if let Some(ext) = &self 8 | .instance 9 | .exts() 10 | .khr_win32_convert_performance_counter_time 11 | { 12 | let mut xr_time = openxr::Time::from_nanos(0); 13 | let performance_counter = get_performance_counter().unwrap(); 14 | match unsafe { 15 | (ext.convert_win32_performance_counter_to_time)( 16 | self.instance.as_raw(), 17 | &performance_counter, 18 | &mut xr_time, 19 | ) 20 | } { 21 | openxr::sys::Result::SUCCESS => Ok(xr_time), 22 | _ => Err(anyhow::anyhow!( 23 | "OpenXR convert_win32_performance_counter_to_time failed." 24 | ) 25 | .into()), 26 | } 27 | } else { 28 | Err(anyhow::anyhow!( 29 | "OpenXR extension khr_win32_convert_performance_counter_time needs to be enabled. \ 30 | Enable it via XrContextBuilder::required_extensions()." 31 | ) 32 | .into()) 33 | } 34 | } 35 | } 36 | 37 | #[cfg(target_os = "windows")] 38 | fn get_performance_counter() -> Result { 39 | unsafe { 40 | let mut time = 0; 41 | windows::Win32::System::Performance::QueryPerformanceCounter(&mut time).ok()?; 42 | Ok(time) 43 | } 44 | } 45 | 46 | #[cfg(not(target_os = "windows"))] 47 | impl XrContext { 48 | pub fn now(&self) -> HothamResult { 49 | if let Some(ext) = &self.instance.exts().khr_convert_timespec_time { 50 | let mut xr_time = openxr::Time::from_nanos(0); 51 | let timespec_time = now_monotonic(); 52 | match unsafe { 53 | (ext.convert_timespec_time_to_time)( 54 | self.instance.as_raw(), 55 | ×pec_time, 56 | &mut xr_time, 57 | ) 58 | } { 59 | openxr::sys::Result::SUCCESS => Ok(xr_time), 60 | _ => Err(anyhow::anyhow!("OpenXR convert_timespec_time_to_time failed.").into()), 61 | } 62 | } else { 63 | Err(anyhow::anyhow!( 64 | "OpenXR extension khr_convert_timespec_time needs to be enabled. \ 65 | Enable it via XrContextBuilder::required_extensions()." 66 | ) 67 | .into()) 68 | } 69 | } 70 | } 71 | 72 | #[cfg(not(target_os = "windows"))] 73 | fn now_monotonic() -> libc::timespec { 74 | let mut time = libc::timespec { 75 | tv_sec: 0, 76 | tv_nsec: 0, 77 | }; 78 | let ret = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut time) }; 79 | assert!(ret == 0); 80 | time 81 | } 82 | -------------------------------------------------------------------------------- /hotham/src/hotham_error.rs: -------------------------------------------------------------------------------- 1 | use ash::vk::Result as VulkanResult; 2 | use openxr::sys::Result as OpenXRResult; 3 | use thiserror::Error; 4 | 5 | /// Hotham Error type 6 | #[derive(Error, Debug)] 7 | pub enum HothamError { 8 | /// OpenXR error 9 | #[error("There was a problem with an OpenXR operation")] 10 | OpenXRError(#[from] OpenXRResult), 11 | /// Vulkan error 12 | #[error("There was a problem with a Vulkan operation")] 13 | VulkanError(#[from] VulkanResult), 14 | /// An empty list // TODO: useless 15 | #[error("The list was empty")] 16 | EmptyListError, 17 | /// Unsupported version 18 | #[error("the version of vulkan or openxr is not supported")] 19 | UnsupportedVersionError, 20 | /// Invalid format 21 | #[error("The format provided - {format:?} - is not supported for this operation")] 22 | InvalidFormatError { 23 | /// The format that was invalid 24 | format: String, 25 | }, 26 | /// Engine shutting down 27 | #[error("The engine is shutting down")] 28 | ShuttingDown, 29 | /// IO error 30 | #[error(transparent)] 31 | IO(#[from] std::io::Error), 32 | /// Not rendering yet 33 | #[error("this session is not rendering yet")] 34 | NotRendering, 35 | /// Some other error 36 | #[error(transparent)] 37 | Other(#[from] anyhow::Error), 38 | } 39 | -------------------------------------------------------------------------------- /hotham/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | // TODO Safety doc would be nice 3 | #![allow(clippy::missing_safety_doc)] 4 | 5 | //! G'day, and welcome to Hotham! 👋 6 | //! 7 | //! Hotham is an attempt to create a lightweight, high performance game engine for mobile VR headsets. It's primarily aimed at small (1-5 person) teams of mostly technical folk who are looking to create games for devices like the Oculus Quest, but find existing tools cumbersome to work with. You can learn more about the project [in the FAQ](https://github.com/leetvr/hotham/wiki/FAQ). 8 | //! 9 | //! # Getting started 10 | //! Hotham is a complex project with many moving parts! Have no fear - we've written an easy to follow [Getting Started guide](https://github.com/leetvr/hotham/wiki/Getting-started) that will have you running our example application in no time. Head on over to [getting started](https://github.com/leetvr/hotham/wiki/Getting-started) to.. get.. started. 11 | //! 12 | //! # Sponsoring 13 | //! Hotham's development is only possible thanks to the support of the community. It's currently being developed on full time by [@kanerogers](https://github.com/kanerogers) If you'd like to help make VR development in Rust possible, please [consider becoming a donor](https://github.com/sponsors/leetvr). 💗 14 | 15 | pub use anyhow; 16 | pub use ash; 17 | pub use ash::vk; 18 | pub use openxr as xr; 19 | pub use vk_shader_macros; 20 | 21 | pub use engine::{Engine, EngineBuilder, TickData}; 22 | pub use glam; 23 | pub use hecs; 24 | pub use hotham_error::HothamError; 25 | pub use id_arena; 26 | pub use rapier3d::na; 27 | 28 | /// Components are data that are used to update the simulation and interact with the external world 29 | pub mod components; 30 | mod engine; 31 | 32 | /// A tool to import models from glTF files into Hotham 33 | pub mod asset_importer; 34 | /// Contexts are wrappers around some external state that the engine will interact with 35 | pub mod contexts; 36 | mod hotham_error; 37 | /// Systems are functions called each frame to update either the external state or the current simulation 38 | pub mod systems; 39 | 40 | /// Kitchen sink utility functions 41 | pub mod util; 42 | 43 | /// Functionality used by the rendering engine 44 | pub mod rendering; 45 | mod workers; 46 | 47 | /// Hotham result type 48 | pub type HothamResult = std::result::Result; 49 | 50 | /// Format used for color textures 51 | pub const COLOR_FORMAT: vk::Format = vk::Format::R8G8B8A8_SRGB; 52 | /// Format used for depth textures 53 | pub const DEPTH_FORMAT: vk::Format = vk::Format::D32_SFLOAT; 54 | 55 | /// Number of views 56 | pub const VIEW_COUNT: u32 = 2; 57 | 58 | /// Swapchain length 59 | pub const SWAPCHAIN_LENGTH: usize = 3; 60 | 61 | /// OpenXR view type 62 | pub const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO; 63 | 64 | /// OpenXR blend mode 65 | pub const BLEND_MODE: xr::EnvironmentBlendMode = xr::EnvironmentBlendMode::OPAQUE; 66 | -------------------------------------------------------------------------------- /hotham/src/rendering/camera.rs: -------------------------------------------------------------------------------- 1 | use glam::{Affine3A, Mat4, Vec4}; 2 | use openxr as xr; 3 | 4 | use crate::util::affine_from_posef; 5 | 6 | #[derive(Debug, Clone)] 7 | /// The Camera, or View, in a scene. 8 | pub struct Camera { 9 | /// The camera's pose in globally oriented stage space 10 | pub gos_from_view: Affine3A, 11 | /// The view matrix 12 | pub view_from_gos: Mat4, 13 | } 14 | 15 | impl Default for Camera { 16 | fn default() -> Self { 17 | Self { 18 | gos_from_view: Affine3A::IDENTITY, 19 | view_from_gos: Mat4::IDENTITY, 20 | } 21 | } 22 | } 23 | 24 | impl Camera { 25 | /// Update the camera's position from an OpenXR view 26 | pub fn update(&mut self, view: &xr::View, gos_from_stage: &Affine3A) -> Mat4 { 27 | // Convert values from OpenXR format and use globally oriented stage space instead of stage space 28 | let stage_from_view = affine_from_posef(view.pose); 29 | self.gos_from_view = *gos_from_stage * stage_from_view; 30 | 31 | self.view_from_gos = self.build_matrix(); 32 | self.view_from_gos 33 | } 34 | 35 | /// Get the camera's position in homogenous coordinates 36 | pub fn position_in_gos(&self) -> Vec4 { 37 | let p = self.gos_from_view.translation; 38 | [p[0], p[1], p[2], 1.].into() 39 | } 40 | 41 | /// Build the camera's view matrix 42 | pub fn build_matrix(&self) -> Mat4 { 43 | self.gos_from_view.inverse().into() 44 | } 45 | } 46 | 47 | #[derive(Debug, Copy, Clone)] 48 | /// A frustrum for the virtual camera. 49 | pub struct Frustum { 50 | /// The left angle 51 | pub left: f32, 52 | /// The right angle 53 | pub right: f32, 54 | /// The top angle 55 | pub up: f32, 56 | /// The bottom angle 57 | pub down: f32, 58 | } 59 | 60 | impl Frustum { 61 | #[rustfmt::skip] 62 | /// Compute right-handed y-up inverse Z perspective projection matrix 63 | pub fn projection(&self, znear: f32) -> Mat4 { 64 | // Based on http://dev.theomader.com/depth-precision/ + OpenVR docs 65 | let left = self.left.tan(); 66 | let right = self.right.tan(); 67 | let down = self.down.tan(); 68 | let up = self.up.tan(); 69 | let idx = 1.0 / (right - left); 70 | let idy = 1.0 / (down - up); 71 | let sx = right + left; 72 | let sy = down + up; 73 | 74 | // TODO: This was originally written using nalgebra's row-order format, so we just 75 | // transpose the resulting matrix. We should probably just.. you know, rewrite this. 76 | Mat4::from_cols_array(&[ 77 | 2.0 * idx, 0.0, sx * idx, 0.0, 78 | 0.0, 2.0 * idy, sy * idy, 0.0, 79 | 0.0, 0.0, 0.0, znear, 80 | 0.0, 0.0, -1.0, 0.0]).transpose() 81 | } 82 | } 83 | 84 | impl From for Frustum { 85 | fn from(x: xr::Fovf) -> Self { 86 | Self { 87 | left: x.angle_left, 88 | right: x.angle_right, 89 | up: x.angle_up, 90 | down: x.angle_down, 91 | } 92 | } 93 | } 94 | 95 | /// Normals of the clipping planes are pointing towards the inside of the frustum. 96 | /// We are only using four planes per camera. The near and far planes are not used. 97 | /// This link points to a paper describing the math behind these expressions: 98 | /// https://www.gamedevs.org/uploads/fast-extraction-viewing-frustum-planes-from-world-view-projection-matrix.pdf 99 | pub(crate) fn extract_planes_from_frustum(frustum: &Mat4) -> Mat4 { 100 | // Glam doesn't support creating matrices from rows, so we'll just use columns and transpose. 101 | Mat4::from_cols( 102 | normalize_plane(frustum.row(3) + frustum.row(0)), 103 | normalize_plane(frustum.row(3) - frustum.row(0)), 104 | normalize_plane(frustum.row(3) + frustum.row(1)), 105 | normalize_plane(frustum.row(3) - frustum.row(1)), 106 | ) 107 | .transpose() 108 | } 109 | 110 | pub(crate) fn normalize_plane(p: Vec4) -> Vec4 { 111 | p / p.truncate().length() 112 | } 113 | -------------------------------------------------------------------------------- /hotham/src/rendering/frame.rs: -------------------------------------------------------------------------------- 1 | use ash::vk; 2 | 3 | use crate::contexts::{render_context::CullParams, VulkanContext}; 4 | use anyhow::Result; 5 | 6 | use super::{ 7 | buffer::Buffer, 8 | descriptors::{ 9 | Descriptors, CULL_PARAMS_BINDING, DRAW_DATA_BINDING, PRIMITIVE_CULL_DATA_BINDING, 10 | SCENE_DATA_BINDING, 11 | }, 12 | resources::{DrawData, PrimitiveCullData}, 13 | scene_data::SceneData, 14 | }; 15 | 16 | // We *can* draw this many objects, but.. seriously? 17 | static DRAW_DATA_BUFFER_SIZE: usize = 5000; 18 | 19 | // We *can* draw this many objects, but.. seriously? 20 | static PRIMITIVE_CULL_DATA_BUFFER_SIZE: usize = 100_000; 21 | 22 | /// A container for all the resources necessary to render a single frame. 23 | #[derive(Debug, Clone)] 24 | pub struct Frame { 25 | /// The fence used to signal when the frame has completed rendering 26 | pub fence: vk::Fence, 27 | /// A command buffer used to record commands 28 | pub command_buffer: vk::CommandBuffer, 29 | /// The fence used to signal when the frame has completed rendering 30 | pub compute_fence: vk::Fence, 31 | /// A command buffer used to record commands 32 | pub compute_command_buffer: vk::CommandBuffer, 33 | /// Data for the primitives that will be drawn this frame, indexed by gl_InstanceId 34 | pub draw_data_buffer: Buffer, 35 | /// The actual draw calls for this frame. 36 | pub primitive_cull_data_buffer: Buffer, 37 | /// Shared data used in a scene 38 | pub scene_data_buffer: Buffer, 39 | /// Shared data used in a scene 40 | pub cull_params_buffer: Buffer, 41 | } 42 | 43 | impl Frame { 44 | pub(crate) fn new( 45 | vulkan_context: &VulkanContext, 46 | index: usize, 47 | descriptors: &Descriptors, 48 | ) -> Result { 49 | let device = &vulkan_context.device; 50 | let command_pool = vulkan_context.command_pool; 51 | 52 | let fence = unsafe { 53 | device.create_fence( 54 | &vk::FenceCreateInfo::builder().flags(vk::FenceCreateFlags::SIGNALED), 55 | None, 56 | ) 57 | }?; 58 | let compute_fence = unsafe { device.create_fence(&vk::FenceCreateInfo::builder(), None) }?; 59 | 60 | let command_buffers = unsafe { 61 | device.allocate_command_buffers( 62 | &vk::CommandBufferAllocateInfo::builder() 63 | .command_buffer_count(2) 64 | .level(vk::CommandBufferLevel::PRIMARY) 65 | .command_pool(command_pool), 66 | ) 67 | }?; 68 | 69 | let command_buffer = command_buffers[0]; 70 | let compute_command_buffer = command_buffers[1]; 71 | 72 | let draw_data_buffer = unsafe { 73 | Buffer::new( 74 | vulkan_context, 75 | vk::BufferUsageFlags::STORAGE_BUFFER, 76 | DRAW_DATA_BUFFER_SIZE, 77 | ) 78 | }; 79 | let primitive_cull_data_buffer = unsafe { 80 | Buffer::new( 81 | vulkan_context, 82 | vk::BufferUsageFlags::STORAGE_BUFFER, 83 | PRIMITIVE_CULL_DATA_BUFFER_SIZE, 84 | ) 85 | }; 86 | let mut scene_data_buffer = 87 | unsafe { Buffer::new(vulkan_context, vk::BufferUsageFlags::UNIFORM_BUFFER, 1) }; 88 | let cull_params_buffer = 89 | unsafe { Buffer::new(vulkan_context, vk::BufferUsageFlags::UNIFORM_BUFFER, 1) }; 90 | 91 | // Update the descriptor sets for this frame. 92 | unsafe { 93 | // Graphics 94 | draw_data_buffer.update_descriptor_set( 95 | &vulkan_context.device, 96 | descriptors.sets[index], 97 | DRAW_DATA_BINDING, 98 | ); 99 | scene_data_buffer.update_descriptor_set( 100 | &vulkan_context.device, 101 | descriptors.sets[index], 102 | SCENE_DATA_BINDING, 103 | ); 104 | 105 | // Compute 106 | primitive_cull_data_buffer.update_descriptor_set( 107 | &vulkan_context.device, 108 | descriptors.compute_sets[index], 109 | PRIMITIVE_CULL_DATA_BINDING, 110 | ); 111 | cull_params_buffer.update_descriptor_set( 112 | &vulkan_context.device, 113 | descriptors.compute_sets[index], 114 | CULL_PARAMS_BINDING, 115 | ); 116 | 117 | // Add some default data to the scene buffer. 118 | scene_data_buffer.push(&Default::default()); 119 | } 120 | 121 | Ok(Self { 122 | fence, 123 | compute_fence, 124 | command_buffer, 125 | compute_command_buffer, 126 | draw_data_buffer, 127 | primitive_cull_data_buffer, 128 | scene_data_buffer, 129 | cull_params_buffer, 130 | }) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /hotham/src/rendering/image.rs: -------------------------------------------------------------------------------- 1 | use ash::vk; 2 | 3 | /// Thin wrapper around a locally created Vulkan image. 4 | #[derive(Debug, Clone)] 5 | pub struct Image { 6 | /// Handle to the underlying Vulkan image 7 | pub handle: vk::Image, 8 | /// Handle to an ImageView for this image 9 | pub view: vk::ImageView, 10 | /// Handle to the underlying image 11 | pub device_memory: vk::DeviceMemory, 12 | /// The extent (size) of this image 13 | pub extent: vk::Extent2D, 14 | /// Flags to indicate to the renderer how this image will be used 15 | pub usage: vk::ImageUsageFlags, 16 | /// The format of this image 17 | pub format: vk::Format, 18 | /// The type of ImageView 19 | pub view_type: vk::ImageViewType, 20 | /// The number of layers in the image 21 | pub layer_count: u32, 22 | } 23 | 24 | impl Image { 25 | /// Create a new image 26 | /// TODO: At the moment the "logic" for this function is handled by `vulkan_context`. We should change that. 27 | #[cfg_attr(feature = "cargo-clippy", allow(clippy::too_many_arguments))] 28 | pub fn new( 29 | handle: vk::Image, 30 | view: vk::ImageView, 31 | device_memory: vk::DeviceMemory, 32 | extent: vk::Extent2D, 33 | usage: vk::ImageUsageFlags, 34 | format: vk::Format, 35 | view_type: vk::ImageViewType, 36 | layer_count: u32, 37 | ) -> Self { 38 | Self { 39 | handle, 40 | view, 41 | device_memory, 42 | extent, 43 | usage, 44 | format, 45 | view_type, 46 | layer_count, 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /hotham/src/rendering/legacy_buffer.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | 3 | use anyhow::Result; 4 | use ash::vk; 5 | use std::marker::PhantomData; 6 | 7 | use crate::contexts::VulkanContext; 8 | 9 | /// A thin wrapper around `vk::Buffer` 10 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] 11 | #[deprecated] 12 | pub struct Buffer { 13 | /// Handle to the underlying buffer 14 | pub handle: vk::Buffer, 15 | /// Handle to the underlying memory 16 | pub device_memory: vk::DeviceMemory, 17 | /// Marker for the type 18 | pub _phantom: PhantomData, 19 | /// Size of the data 20 | pub size: vk::DeviceSize, 21 | /// Size of the underlying buffer 22 | pub device_memory_size: vk::DeviceSize, 23 | /// Usage flags for the buffer 24 | pub usage: vk::BufferUsageFlags, 25 | } 26 | 27 | impl Buffer 28 | where 29 | T: Sized + Copy, 30 | { 31 | /// Create a new buffer from a slice of data. 32 | pub fn new( 33 | vulkan_context: &VulkanContext, 34 | data: &[T], 35 | usage: vk::BufferUsageFlags, 36 | ) -> Result { 37 | let size = std::mem::size_of_val(data) as vk::DeviceSize; 38 | let (handle, device_memory, device_memory_size) = 39 | vulkan_context.create_buffer_with_data(data, usage, size)?; 40 | 41 | Ok(Self { 42 | handle, 43 | device_memory, 44 | size, 45 | device_memory_size, 46 | usage, 47 | _phantom: PhantomData, 48 | }) 49 | } 50 | 51 | /// **NOTE**: If passing in a Vec, you MUST use vec.as_ptr(), passing in 52 | /// a reference will result in A Very Bad Time. 53 | pub fn update(&self, vulkan_context: &VulkanContext, data: &[T]) -> Result<()> { 54 | vulkan_context.update_buffer( 55 | data, 56 | self.device_memory, 57 | self.device_memory_size, 58 | self.usage, 59 | ) 60 | } 61 | } 62 | 63 | // TODO: Need to be able to drop Buffers 64 | // impl Buffer { 65 | // pub(crate) fn destroy(&self, vulkan_context: &VulkanContext) -> () { 66 | // let device = &vulkan_context.device; 67 | // unsafe { 68 | // device.destroy_buffer(self.handle, None); 69 | // device.free_memory(self.device_memory, None); 70 | // }; 71 | // } 72 | // } 73 | -------------------------------------------------------------------------------- /hotham/src/rendering/memory.rs: -------------------------------------------------------------------------------- 1 | use crate::contexts::VulkanContext; 2 | use ash::vk; 3 | 4 | pub(crate) unsafe fn allocate_memory( 5 | vulkan_context: &VulkanContext, 6 | memory_requirements: vk::MemoryRequirements, 7 | memory_property_flags: vk::MemoryPropertyFlags, 8 | ) -> vk::DeviceMemory { 9 | let instance = &vulkan_context.instance; 10 | let device = &vulkan_context.device; 11 | let physical_device = vulkan_context.physical_device; 12 | 13 | let memory_type_bits_requirement = memory_requirements.memory_type_bits; 14 | let memory_properties = instance.get_physical_device_memory_properties(physical_device); 15 | let memory_type_index = find_memory_type_index( 16 | memory_properties, 17 | memory_type_bits_requirement, 18 | memory_property_flags, 19 | ); 20 | println!("[HOTHAM_VULKAN] Using memory type {memory_type_index}"); 21 | device 22 | .allocate_memory( 23 | &vk::MemoryAllocateInfo::builder() 24 | .allocation_size(memory_requirements.size) 25 | .memory_type_index(memory_type_index as _), 26 | None, 27 | ) 28 | .unwrap() 29 | } 30 | 31 | fn find_memory_type_index( 32 | memory_properties: vk::PhysicalDeviceMemoryProperties, 33 | memory_type_bits_requirement: u32, 34 | memory_property_flags: vk::MemoryPropertyFlags, 35 | ) -> usize { 36 | let mut memory_type_index = !0; 37 | for memory_index in 0..memory_properties.memory_type_count as usize { 38 | let memory_type_bits: u32 = 1 << memory_index; 39 | let is_required_memory_type = (memory_type_bits_requirement & memory_type_bits) != 0; 40 | let properties = memory_properties.memory_types[memory_index].property_flags; 41 | 42 | if is_required_memory_type && properties.contains(memory_property_flags) { 43 | memory_type_index = memory_index; 44 | break; 45 | } 46 | } 47 | if memory_type_index == !0 { 48 | panic!("Unable to find suitable memory!") 49 | } 50 | memory_type_index 51 | } 52 | -------------------------------------------------------------------------------- /hotham/src/rendering/mesh_data.rs: -------------------------------------------------------------------------------- 1 | use crate::{asset_importer::ImportContext, rendering::primitive::Primitive}; 2 | /// Wrapper that encapsulates geometry. Maps closely to the [glTF spec](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#meshes) 3 | /// Usually automatically added by `gltf_loader`. 4 | #[derive(Debug, Clone, PartialEq)] 5 | pub struct MeshData { 6 | /// The primitives in this mesh (eg. actual geometry) 7 | pub primitives: Vec, 8 | } 9 | 10 | impl MeshData { 11 | /// Create new MeshData from a list of Primitives 12 | pub fn new(primitives: Vec) -> Self { 13 | Self { primitives } 14 | } 15 | 16 | /// Takes mesh data from a glTF file, uploads it to the GPU and inserts it into mesh_map 17 | pub(crate) fn load(mesh: gltf::Mesh, import_context: &mut ImportContext) -> Self { 18 | let mesh_name = mesh 19 | .name() 20 | .map(|s| s.to_string()) 21 | .unwrap_or(format!("Mesh {}", mesh.index())); 22 | 23 | let primitives = mesh 24 | .primitives() 25 | .map(|p| Primitive::load(p, import_context, &mesh_name)) 26 | .collect::>(); 27 | 28 | MeshData { primitives } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /hotham/src/rendering/mod.rs: -------------------------------------------------------------------------------- 1 | /// Legacy wrapper around GPU buffers 2 | pub mod legacy_buffer; 3 | 4 | /// Buffers are used to transfer data to the GPU 5 | pub mod buffer; 6 | 7 | /// Functionality for interacting with GPU memory 8 | pub mod memory; 9 | 10 | /// The virtual camera 11 | pub mod camera; 12 | 13 | /// A wrapper around the frame-dependent resources 14 | pub mod frame; 15 | 16 | /// A wrapper around an image 17 | pub mod image; 18 | 19 | /// Shared data for a scene 20 | pub mod scene_data; 21 | 22 | /// Helper wrapper for interacting with the swapchain 23 | pub mod swapchain; 24 | 25 | /// Geometry 26 | pub mod primitive; 27 | 28 | /// Functionality for adding textures (images) to meshes 29 | pub mod texture; 30 | 31 | /// Vertex representation 32 | pub mod vertex; 33 | 34 | /// A wrapper for all Descriptor related functionality 35 | pub(crate) mod descriptors; 36 | 37 | /// Container for all the resources used to render objects 38 | pub mod resources; 39 | 40 | /// Data to instruct the renderer how a primitive should look 41 | pub mod material; 42 | 43 | /// Lights and related functionality 44 | pub mod light; 45 | /// Wrapper around geometry data. 46 | pub mod mesh_data; 47 | -------------------------------------------------------------------------------- /hotham/src/rendering/scene_data.rs: -------------------------------------------------------------------------------- 1 | use glam::{Mat4, Vec4}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::light::{Light, MAX_LIGHTS}; 5 | 6 | /// The amount of Image Based Lighting (IBL) to show in the scene 7 | pub const DEFAULT_IBL_INTENSITY: f32 = 1.0; 8 | 9 | /// Data about the current scene. Sent to the vertex and fragment shaders 10 | #[derive(Deserialize, Serialize, Clone, Debug)] 11 | #[repr(C)] 12 | pub struct SceneData { 13 | /// View-Projection matrices (one per eye) 14 | pub view_projection: [Mat4; 2], 15 | /// Position of the cameras (one per eye) 16 | pub camera_position: [Vec4; 2], 17 | /// Scene Parameters - x = IBL intensity, y = unused, z = debug render inputs, w = debug render algorithm 18 | pub params: Vec4, 19 | /// Dynamic punctual lights 20 | pub lights: [Light; MAX_LIGHTS], 21 | } 22 | 23 | impl Default for SceneData { 24 | fn default() -> Self { 25 | Self { 26 | view_projection: [Mat4::IDENTITY, Mat4::IDENTITY], 27 | camera_position: [Vec4::ZERO, Vec4::ZERO], 28 | params: [DEFAULT_IBL_INTENSITY, 0., 0., 0.].into(), 29 | lights: [Light::none(), Light::none(), Light::none(), Light::none()], 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /hotham/src/rendering/vertex.rs: -------------------------------------------------------------------------------- 1 | use ash::vk; 2 | use glam::{Vec2, Vec3, Vec4}; 3 | // const VERTEX_FORMAT: vk::Format = vk::Format::R16G16B16_SFLOAT; 4 | const VERTEX_FORMAT: vk::Format = vk::Format::R32G32B32_SFLOAT; 5 | 6 | /// Representation of a single vertex, usually imported from a glTF file. 7 | #[repr(C)] 8 | #[derive(Clone, Debug, Copy, PartialEq, Default)] 9 | pub struct Vertex { 10 | // /// Position in model space 11 | // pub position: Vec3, 12 | /// Normal in model space 13 | pub normal: Vec3, 14 | /// First set of texture coordinates 15 | pub texture_coords: Vec2, 16 | /// Joint indices (for skinning), one byte per index. 17 | pub joint_indices: u32, 18 | /// Joint weights (for skinning), one byte per weight. 19 | pub joint_weights: u32, 20 | } 21 | 22 | impl Vertex { 23 | /// Create a new vertex 24 | pub fn new(normal: Vec3, texture_coords: Vec2, joint_indices: u32, joint_weights: u32) -> Self { 25 | Self { 26 | // position, 27 | normal, 28 | texture_coords, 29 | joint_indices, 30 | joint_weights, 31 | } 32 | } 33 | 34 | /// Create a new vertex from a zip - useful when importing from glTF 35 | // Clippy warning suppressed for adjudication separately 36 | #[cfg_attr(feature = "cargo-clippy", allow(clippy::type_complexity))] 37 | pub fn from_zip(t: (Vec3, Vec2, [u8; 4], Vec4)) -> Self { 38 | // Normalize weights to 0 <= w <= 255 while avoiding division with zero. 39 | let max_weight = t.3.max_element().max(f32::EPSILON); 40 | let weight_normalization = 255.0 / max_weight; 41 | Vertex::new( 42 | t.0, 43 | t.1, 44 | // Pack indices into one u32 with one byte per index. 45 | (t.2[0] as u32) 46 | + (t.2[1] as u32) * 256 47 | + (t.2[2] as u32) * 256 * 256 48 | + (t.2[3] as u32) * 256 * 256 * 256, 49 | // Pack weights into one u32 with one byte per weight. 50 | ((t.3[0] * weight_normalization).round() as u32) 51 | + ((t.3[1] * weight_normalization).round() as u32) * 256 52 | + ((t.3[2] * weight_normalization).round() as u32) * 256 * 256 53 | + ((t.3[3] * weight_normalization).round() as u32) * 256 * 256 * 256, 54 | ) 55 | } 56 | } 57 | 58 | impl Vertex { 59 | /// Get the vertex attributes to be used in the Vertex Shader 60 | pub fn attribute_descriptions() -> Vec { 61 | let position = vk::VertexInputAttributeDescription::builder() 62 | .binding(0) 63 | .location(0) 64 | .format(VERTEX_FORMAT) 65 | .offset(0) 66 | .build(); 67 | 68 | let normal = vk::VertexInputAttributeDescription::builder() 69 | .binding(1) 70 | .location(1) 71 | .format(vk::Format::R32G32B32_SFLOAT) 72 | .offset(memoffset::offset_of!(Vertex, normal) as _) 73 | .build(); 74 | 75 | let texture_coords = vk::VertexInputAttributeDescription::builder() 76 | .binding(1) 77 | .location(2) 78 | .format(vk::Format::R32G32_SFLOAT) 79 | .offset(memoffset::offset_of!(Vertex, texture_coords) as _) 80 | .build(); 81 | 82 | let joint_indices = vk::VertexInputAttributeDescription::builder() 83 | .binding(1) 84 | .location(3) 85 | .format(vk::Format::R32_UINT) 86 | .offset(memoffset::offset_of!(Vertex, joint_indices) as _) 87 | .build(); 88 | 89 | let joint_weights = vk::VertexInputAttributeDescription::builder() 90 | .binding(1) 91 | .location(4) 92 | .format(vk::Format::R32_UINT) 93 | .offset(memoffset::offset_of!(Vertex, joint_weights) as _) 94 | .build(); 95 | 96 | vec![ 97 | position, 98 | normal, 99 | texture_coords, 100 | joint_indices, 101 | joint_weights, 102 | ] 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /hotham/src/shaders/brdf.glsl: -------------------------------------------------------------------------------- 1 | // Filament's lambertian term is 1.0 / PI, so we just pre-compute that 2 | #define BRDF_LAMBERTIAN F16(0.3183098862) 3 | 4 | // The following equation models the Fresnel reflectance term of the spec equation (aka F()) 5 | // Implementation of fresnel from [4], Equation 15 6 | 7 | // Anything less than 2% is physically impossible and is instead considered to be shadowing. Compare to "Real-Time-Rendering" 4th edition on page 325. 8 | const f16vec3 f90 = V16(1.0); 9 | 10 | // f16vec3 F_Schlick(f16vec3 f0, float16_tVdotH) { 11 | // return f0 + (f90 - f0) * pow(clamp(1.0 - VdotH, 0.0, 1.0), 5.0); 12 | // } 13 | 14 | // Smith Joint GGX 15 | // Note: Vis = G / (4 * NdotL * NdotV) 16 | // see Eric Heitz. 2014. Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs. Journal of Computer Graphics Techniques, 3 17 | // see Real-Time Rendering. Page 331 to 336. 18 | // see https://google.github.io/filament/Filament.md.html#materialsystem/specularbrdf/geometricshadowing(specularg) 19 | // float16_t V_GGX(float16_t NdotL, float16_t NdotV, float16_t alphaRoughness) { 20 | // float16_t alphaRoughnessSq = alphaRoughness * alphaRoughness; 21 | 22 | // float16_t GGXV = NdotL * sqrt(NdotV * NdotV * (1.0 - alphaRoughnessSq) + alphaRoughnessSq); 23 | // float16_t GGXL = NdotV * sqrt(NdotL * NdotL * (1.0 - alphaRoughnessSq) + alphaRoughnessSq); 24 | 25 | // float16_t GGX = GGXV + GGXL; 26 | // if (GGX > 0.0) { 27 | // return 0.5 / GGX; 28 | // } 29 | // return 0.0; 30 | // } 31 | 32 | float16_t D_GGX(float16_t roughness, f16vec3 n, float16_t NdotH, f16vec3 h) { 33 | f16vec3 NxH = cross(n, h); 34 | float16_t oneMinusNoHSquared = dot(NxH, NxH); 35 | 36 | float16_t a = NdotH * roughness; 37 | float16_t k = roughness / (oneMinusNoHSquared + a * a); 38 | float16_t d = k * k * BRDF_LAMBERTIAN; 39 | return saturateMediump(d); 40 | } 41 | 42 | float16_t V_SmithGGXCorrelated_Fast(float16_t roughness, float16_t NdotV, float16_t NdotL) { 43 | // Hammon 2017, "PBR Diffuse Lighting for GGX+Smith Microsurfaces" 44 | float16_t v = F16(0.5) / mix(F16(2.0) * NdotL * NdotV, NdotL + NdotV, roughness); 45 | return saturateMediump(v); 46 | } 47 | 48 | // KR: why is this VdotH instead of LdotH? 49 | f16vec3 F_Schlick(const f16vec3 f0, float16_t VdotH) { 50 | float16_t f = pow(F16(1.0) - VdotH, F16(5.0)); 51 | return f + f0 * (F16(1.0) - f); 52 | } 53 | 54 | // // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#acknowledgments AppendixB 55 | // f16vec3 BRDF_specularGGX(f16vec3 f0, float16_talphaRoughness, float16_tVdotH, float16_tNdotL, float16_tNdotV, float16_tNdotH) { 56 | // f16vec3 F = F_Schlick(f0, VdotH); 57 | // float16_tVis = V_GGX(NdotL, NdotV, alphaRoughness); 58 | // float16_tD = D_GGX(NdotH, alphaRoughness); 59 | 60 | // return F * Vis * D; 61 | // } 62 | 63 | f16vec3 BRDF_specular(const f16vec3 f0, const float16_t roughness, f16vec3 h, const f16vec3 n, float16_t NdotV, float16_t NdotL, float16_t NdotH, float16_t LdotH) { 64 | float16_t D = D_GGX(roughness, n, NdotH, h); 65 | float16_t V = V_SmithGGXCorrelated_Fast(roughness, NdotV, NdotL); 66 | f16vec3 F = F_Schlick(f0, LdotH); 67 | 68 | return (D * V) * F; 69 | } 70 | -------------------------------------------------------------------------------- /hotham/src/shaders/common.glsl: -------------------------------------------------------------------------------- 1 | #extension GL_ARB_separate_shader_objects : enable 2 | #extension GL_EXT_multiview : enable 3 | 4 | #define NOT_PRESENT 4294967295 5 | #define MAX_JOINTS 64 6 | 7 | #define PI F16(3.14159265359) 8 | #define HALF_PI F16(1.570796327) 9 | #define MEDIUMP_FLT_MAX F16(65504.0) 10 | #define MEDIUMP_FLT_MIN F16(0.00006103515625) 11 | #define saturateMediump(x) min(x, MEDIUMP_FLT_MAX) 12 | #define F16(x) float16_t(x) 13 | #define V16(x) f16vec3(x) 14 | #define saturate(x) clamp(x, F16(0), F16(1)) 15 | 16 | // Representation of a light in a scene, based on the KHR_lights_punctual extension: 17 | // https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual 18 | // TODO: make these f16 19 | struct Light { 20 | vec3 direction; 21 | float falloff; 22 | 23 | vec3 color; 24 | float intensity; 25 | 26 | vec3 position; 27 | float lightAngleScale; 28 | 29 | float lightAngleOffset; 30 | uint type; 31 | }; 32 | 33 | const uint LightType_Directional = 0; 34 | const uint LightType_Point = 1; 35 | const uint LightType_Spot = 2; 36 | 37 | layout (set = 0, binding = 2) readonly uniform SceneData { 38 | mat4 viewProjection[2]; 39 | vec4 cameraPosition[2]; 40 | vec4 params; 41 | Light lights[4]; 42 | } sceneData; 43 | -------------------------------------------------------------------------------- /hotham/src/shaders/culling.comp: -------------------------------------------------------------------------------- 1 | #version 460 2 | 3 | layout (local_size_x = 1024) in; 4 | struct VkDrawIndexedIndirectCommand { 5 | uint indexCount; 6 | uint instanceCount; 7 | uint firstIndex; 8 | int vertexOffset; 9 | uint firstInstance; 10 | }; 11 | 12 | struct PrimitiveCullData { 13 | vec4 boundingSphere; 14 | uint indexInstance; 15 | uint indexOffset; 16 | bool visible; 17 | }; 18 | 19 | layout(std430, set = 0, binding = 0) buffer block { 20 | PrimitiveCullData data[]; 21 | } primitiveCullDataBuffer; 22 | 23 | layout(set = 0, binding = 1) uniform CullData { 24 | mat4 leftClipPlanes; 25 | mat4 rightClipPlanes; 26 | uint drawCalls; 27 | } cullData; 28 | 29 | void main() { 30 | uint id = gl_GlobalInvocationID.x; 31 | 32 | if (id >= cullData.drawCalls) { return; } 33 | 34 | PrimitiveCullData d = primitiveCullDataBuffer.data[id]; 35 | vec4 center4 = vec4(d.boundingSphere.xyz, 1); 36 | vec4 negRadius4 = -d.boundingSphere.wwww; 37 | 38 | // Perform a plane intersection check against each eye's clip plane. 39 | // If the primitive is visible in either eye, we consider it visible. 40 | primitiveCullDataBuffer.data[id].visible = any(bvec2( 41 | all(greaterThan(cullData.leftClipPlanes * center4, negRadius4)), 42 | all(greaterThan(cullData.rightClipPlanes * center4, negRadius4)) 43 | )); 44 | } 45 | -------------------------------------------------------------------------------- /hotham/src/shaders/gui.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout (location = 0) in vec4 inColor; 4 | layout (location = 1) in vec2 inUV; 5 | 6 | layout (location = 0) out vec4 outColor; 7 | 8 | layout (binding = 0, set = 0) uniform sampler2D font_texture; 9 | 10 | void main() { 11 | outColor = inColor * texture(font_texture, inUV); 12 | } 13 | -------------------------------------------------------------------------------- /hotham/src/shaders/gui.vert: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/MatchaChoco010/egui-winit-ash-integration/blob/main/src/shaders/src/vert.vert 2 | #version 450 3 | 4 | layout (location = 0) in vec2 inPos; 5 | layout (location = 1) in vec2 inUV; 6 | layout (location = 2) in vec4 inColor; 7 | 8 | layout (location = 0) out vec4 outColor; 9 | layout (location = 1) out vec2 outUV; 10 | 11 | layout (push_constant) uniform PushConstants { 12 | vec2 screen_size; 13 | } pushConstants; 14 | 15 | vec3 srgb_to_linear(vec3 srgb) { 16 | bvec3 cutoff = lessThan(srgb, vec3(0.04045)); 17 | vec3 lower = srgb / vec3(12.92); 18 | vec3 higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); 19 | return mix(higher, lower, cutoff); 20 | } 21 | 22 | void main() { 23 | gl_Position = vec4( 24 | 2.0 * inPos.x / pushConstants.screen_size.x - 1.0, 25 | 2.0 * inPos.y / pushConstants.screen_size.y - 1.0, 26 | 0.0, 27 | 1.0); 28 | outColor = vec4(srgb_to_linear(inColor.rgb), inColor.a); 29 | outUV = inUV; 30 | } 31 | -------------------------------------------------------------------------------- /hotham/src/shaders/lights.glsl: -------------------------------------------------------------------------------- 1 | float16_t getSquareFalloffAttenuation(float16_t distanceSquare, float16_t falloff) { 2 | float16_t factor = distanceSquare * falloff; 3 | float16_t smoothFactor = saturate(F16(1.0) - factor * factor); 4 | // // We would normally divide by the square distance here 5 | // // but we do it at the call site 6 | return smoothFactor * smoothFactor; 7 | } 8 | 9 | float16_t getRangeAttenuation(const vec3 pointToLight, float16_t falloff) { 10 | float16_t distanceSquare = F16(dot(pointToLight, pointToLight)); 11 | float16_t attenuation = getSquareFalloffAttenuation(distanceSquare, falloff); 12 | // Assume a punctual light occupies a volume of 1cm to avoid a division by 0 13 | return attenuation / max(distanceSquare, F16(1e-4)); 14 | } 15 | 16 | // https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_lights_punctual/README.md#inner-and-outer-cone-angles 17 | float16_t getSpotAttenuation(vec3 l, vec3 spotDirection, float16_t spotScale, float16_t spotOffset) { 18 | float16_t cd = F16(dot(spotDirection, l)); 19 | float16_t attenuation = saturate(cd * spotScale + spotOffset); 20 | return attenuation * attenuation; 21 | 22 | } 23 | 24 | float16_t getLightAttenuation(Light light, vec3 pointToLight, vec3 l) { 25 | float16_t rangeAttenuation = F16(1.0); 26 | float16_t spotAttenuation = F16(1.0); 27 | 28 | if (light.type != LightType_Directional) { 29 | rangeAttenuation = getRangeAttenuation(pointToLight, F16(light.falloff)); 30 | } 31 | 32 | if (light.type == LightType_Spot) { 33 | spotAttenuation = getSpotAttenuation(l, -light.direction, F16(light.lightAngleScale), F16(light.lightAngleOffset)); 34 | } 35 | 36 | return rangeAttenuation * spotAttenuation; 37 | } 38 | -------------------------------------------------------------------------------- /hotham/src/shaders/pbr.vert: -------------------------------------------------------------------------------- 1 | // PBR shader based on the Khronos glTF-Sample Viewer: 2 | // https://github.com/KhronosGroup/glTF-WebGL-PBR 3 | #version 460 4 | #extension GL_EXT_multiview : enable 5 | 6 | #include "common.glsl" 7 | 8 | layout (location = 0) in vec3 inPos; 9 | layout (location = 1) in vec3 inNormal; 10 | layout (location = 2) in vec2 inUV; 11 | layout (location = 3) in uint inJoint; 12 | layout (location = 4) in uint inWeight; 13 | 14 | layout (location = 0) out vec4 outGosPos; 15 | layout (location = 1) out vec2 outUV; 16 | layout (location = 2) out vec3 outNormal; 17 | 18 | struct DrawData { 19 | mat4 gosFromLocal; 20 | mat4 localFromGos; 21 | uint skinID; 22 | }; 23 | 24 | layout (set = 0, binding = 0) readonly buffer DrawDataBuffer { 25 | DrawData data[5000]; 26 | } drawDataBuffer; 27 | 28 | layout (std430, set = 0, binding = 1) readonly buffer SkinsBuffer { 29 | mat4 jointMatrices[100][64]; 30 | } skinsBuffer; 31 | 32 | out gl_PerVertex { 33 | vec4 gl_Position; 34 | }; 35 | 36 | void main() { 37 | uint skinID = drawDataBuffer.data[gl_InstanceIndex].skinID; 38 | mat4 gosFromLocal = drawDataBuffer.data[gl_InstanceIndex].gosFromLocal; 39 | mat4 localFromGos = drawDataBuffer.data[gl_InstanceIndex].localFromGos; 40 | 41 | if (skinID == NOT_PRESENT) { 42 | // Mesh has no skin 43 | outGosPos = gosFromLocal * vec4(inPos, 1.0); 44 | outNormal = normalize(inNormal * mat3(localFromGos)); 45 | } else { 46 | // Mesh is skinned 47 | // Shift and mask to unpack the individual indices and weights. 48 | // There is no need to divide with the sum of weights because we are using homogenous coordinates. 49 | mat4 skinMatrix = 50 | ((inWeight) & 255) * skinsBuffer.jointMatrices[skinID][(inJoint) & 255] + 51 | ((inWeight >> 8) & 255) * skinsBuffer.jointMatrices[skinID][(inJoint >> 8) & 255] + 52 | ((inWeight >> 16) & 255) * skinsBuffer.jointMatrices[skinID][(inJoint >> 16) & 255] + 53 | ((inWeight >> 24) & 255) * skinsBuffer.jointMatrices[skinID][(inJoint >> 24) & 255]; 54 | 55 | outGosPos = gosFromLocal * skinMatrix * vec4(inPos, 1.0); 56 | outNormal = normalize(mat3(skinMatrix) * inNormal * mat3(localFromGos)); 57 | } 58 | 59 | outUV = inUV; 60 | gl_Position = sceneData.viewProjection[gl_ViewIndex] * outGosPos; 61 | } 62 | -------------------------------------------------------------------------------- /hotham/src/systems/animation.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | components::{animation_controller::AnimationController, LocalTransform}, 3 | Engine, 4 | }; 5 | 6 | /// Animation system 7 | /// Walks through each AnimationController and applies the appropriate animation to its targets. 8 | pub fn animation_system(engine: &mut Engine) { 9 | animation_system_inner(&mut engine.world); 10 | } 11 | 12 | fn animation_system_inner(world: &mut hecs::World) { 13 | for (_, controller) in world.query::<&AnimationController>().iter() { 14 | let blend_from = controller.blend_from; 15 | let blend_to = controller.blend_to; 16 | let blend_amount = controller.blend_amount; 17 | 18 | for target in &controller.targets { 19 | let mut local_transform = world.get::<&mut LocalTransform>(target.target).unwrap(); 20 | local_transform.translation = 21 | target.translations[blend_from].lerp(target.translations[blend_to], blend_amount); 22 | local_transform.rotation = 23 | target.rotations[blend_from].slerp(target.rotations[blend_to], blend_amount); 24 | local_transform.scale = 25 | target.scales[blend_from].lerp(target.scales[blend_to], blend_amount); 26 | } 27 | } 28 | } 29 | 30 | #[cfg(target_os = "windows")] 31 | #[cfg(test)] 32 | mod tests { 33 | use crate::{ 34 | asset_importer::{add_model_to_world, load_models_from_glb}, 35 | contexts::RenderContext, 36 | }; 37 | 38 | use super::*; 39 | #[test] 40 | pub fn animation_test() { 41 | let (mut render_context, vulkan_context) = RenderContext::testing(); 42 | 43 | let data: Vec<&[u8]> = vec![include_bytes!("../../../test_assets/left_hand.glb")]; 44 | let models = load_models_from_glb(&data, &vulkan_context, &mut render_context).unwrap(); 45 | let mut world = hecs::World::new(); 46 | 47 | // Add the left hand 48 | let left_hand = add_model_to_world("Left Hand", &models, &mut world, None).unwrap(); 49 | { 50 | let mut left_hand_controller = 51 | world.get::<&mut AnimationController>(left_hand).unwrap(); 52 | left_hand_controller.blend_from = 0; 53 | left_hand_controller.blend_to = 1; 54 | left_hand_controller.blend_amount = 0.0; 55 | } 56 | 57 | // Collect all the transforms in the world so we can compare them later. 58 | let transforms_before = world 59 | .query_mut::<&LocalTransform>() 60 | .into_iter() 61 | .map(|r| *r.1) 62 | .collect::>(); 63 | 64 | // Run the animation system 65 | animation_system_inner(&mut world); 66 | 67 | // Collect all the transforms after the system has been run. 68 | let transforms_after = world 69 | .query_mut::<&LocalTransform>() 70 | .into_iter() 71 | .map(|r| *r.1) 72 | .collect::>(); 73 | 74 | // Make sure our transforms have been modified! 75 | assert_ne!(transforms_before, transforms_after); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /hotham/src/systems/debug.rs: -------------------------------------------------------------------------------- 1 | use crate::Engine; 2 | 3 | /// A simple system used to assist with debugging the fragment shader. 4 | pub fn debug_system(engine: &mut Engine) { 5 | let input_context = &mut engine.input_context; 6 | let render_context = &mut engine.render_context; 7 | 8 | if input_context.left.x_button_just_pressed() { 9 | let params = &mut render_context.scene_data.params; 10 | params.w = 0.; 11 | params.z = (params.z + 1.) % 7.; 12 | println!("[HOTHAM_DEBUG] params.z is now {}", params.z); 13 | } 14 | 15 | if input_context.left.y_button_just_pressed() { 16 | let params = &mut render_context.scene_data.params; 17 | params.z = 0.; 18 | params.w = (params.w + 1.) % 6.; 19 | println!("[HOTHAM_DEBUG] params.w is now {}", params.w); 20 | } 21 | 22 | if input_context.right.b_button_just_pressed() { 23 | let params = &mut render_context.scene_data.params; 24 | params.x = (params.x + 0.1) % 5.; 25 | println!("[HOTHAM_DEBUG] params.x is now {}", params.x); 26 | } 27 | 28 | if input_context.right.a_button_just_pressed() { 29 | let params = &mut render_context.scene_data.params; 30 | params.x = (params.x + 5. - 0.1) % 5.; 31 | println!("[HOTHAM_DEBUG] params.x is now {}", params.x); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /hotham/src/systems/haptics.rs: -------------------------------------------------------------------------------- 1 | use openxr::{Duration, HapticVibration}; 2 | 3 | use crate::{ 4 | contexts::{HapticContext, XrContext}, 5 | Engine, 6 | }; 7 | static HAPTIC_FREQUENCY: f32 = 400.; 8 | static HAPTIC_DURATION: i64 = 1e+8 as _; // 100ms 9 | 10 | /// Triggers the application of vibrations to the appropriate user input device at prescribed amplitude, frequency, and duration given a Hotham::resources::XrContent and Hotham::resources::HapticContext. 11 | /// 12 | /// During each tick of the Hotham engine, haptic feedback is applied to generate a HapticVibration 13 | /// event which propagates to the appropriate user input device. 14 | /// 15 | /// Basic usage: 16 | /// ```ignore 17 | /// fn tick (...) { 18 | /// apply_haptic_feedback(xr_context, haptic_context) 19 | /// } 20 | /// ``` 21 | pub fn haptics_system(engine: &mut Engine) { 22 | haptics_system_inner(&mut engine.xr_context, &mut engine.haptic_context) 23 | } 24 | 25 | fn haptics_system_inner(xr_context: &mut XrContext, haptic_context: &mut HapticContext) { 26 | let input = &xr_context.input; 27 | 28 | let haptic_duration = Duration::from_nanos(HAPTIC_DURATION); 29 | if haptic_context.left_hand_amplitude_this_frame != 0. { 30 | let event = HapticVibration::new() 31 | .amplitude(haptic_context.left_hand_amplitude_this_frame) 32 | .frequency(HAPTIC_FREQUENCY) 33 | .duration(haptic_duration); 34 | 35 | input 36 | .haptic_feedback_action 37 | .apply_feedback(&xr_context.session, input.left_hand_subaction_path, &event) 38 | .expect("Unable to apply haptic feedback!"); 39 | 40 | // Reset the value 41 | haptic_context.left_hand_amplitude_this_frame = 0.; 42 | } 43 | 44 | if haptic_context.right_hand_amplitude_this_frame != 0. { 45 | let event = HapticVibration::new() 46 | .amplitude(haptic_context.right_hand_amplitude_this_frame) 47 | .frequency(HAPTIC_FREQUENCY) 48 | .duration(haptic_duration); 49 | 50 | input 51 | .haptic_feedback_action 52 | .apply_feedback(&xr_context.session, input.right_hand_subaction_path, &event) 53 | .expect("Unable to apply haptic feedback!"); 54 | 55 | // Reset the value 56 | haptic_context.right_hand_amplitude_this_frame = 0.; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /hotham/src/systems/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | pub mod animation; 3 | pub mod audio; 4 | pub mod debug; 5 | pub mod draw_gui; 6 | pub mod grabbing; 7 | pub mod hands; 8 | pub mod haptics; 9 | pub mod physics; 10 | pub mod pointers; 11 | pub mod rendering; 12 | pub mod skinning; 13 | pub mod update_global_transform; 14 | 15 | pub use animation::animation_system; 16 | pub use audio::audio_system; 17 | pub use draw_gui::draw_gui_system; 18 | pub use grabbing::grabbing_system; 19 | pub use hands::hands_system; 20 | pub use haptics::haptics_system; 21 | pub use physics::physics_system; 22 | pub use pointers::pointers_system; 23 | pub use rendering::rendering_system; 24 | pub use skinning::skinning_system; 25 | pub use update_global_transform::update_global_transform_system; 26 | -------------------------------------------------------------------------------- /hotham/src/systems/skinning.rs: -------------------------------------------------------------------------------- 1 | use hecs::World; 2 | use render_context::RenderContext; 3 | 4 | use crate::{ 5 | components::{GlobalTransform, Skin}, 6 | contexts::render_context, 7 | Engine, 8 | }; 9 | 10 | /// Skinning system 11 | /// Walks through each joint in the system and builds up the `joint_matrices` that will be sent to the vertex shader 12 | pub fn skinning_system(engine: &mut Engine) { 13 | let world = &mut engine.world; 14 | let render_context = &mut engine.render_context; 15 | skinning_system_inner(world, render_context); 16 | } 17 | 18 | fn skinning_system_inner(world: &mut World, render_context: &mut RenderContext) { 19 | for (_, (skin, global_transform)) in world.query::<(&Skin, &GlobalTransform)>().iter() { 20 | let buffer = unsafe { render_context.resources.skins_buffer.as_slice_mut() }; 21 | let joint_matrices = &mut buffer[skin.id as usize]; 22 | let local_from_global = global_transform.0.inverse(); 23 | 24 | for (n, (joint, joint_from_mesh)) in skin 25 | .joints 26 | .iter() 27 | .zip(skin.inverse_bind_matrices.iter()) 28 | .enumerate() 29 | { 30 | let global_from_joint = world.get::<&GlobalTransform>(*joint).unwrap().0; 31 | let local_from_mesh = local_from_global * global_from_joint * *joint_from_mesh; 32 | joint_matrices[n] = local_from_mesh.into(); 33 | } 34 | } 35 | } 36 | 37 | #[cfg(target_os = "windows")] 38 | #[cfg(test)] 39 | mod tests { 40 | 41 | use std::io::Write; 42 | 43 | use crate::{ 44 | components::{Info, Skin}, 45 | util::get_world_with_hands, 46 | }; 47 | 48 | use super::*; 49 | use approx::relative_eq; 50 | use glam::{Affine3A, Mat4}; 51 | 52 | #[test] 53 | pub fn test_hand_skinning() { 54 | let (mut render_context, vulkan_context) = RenderContext::testing(); 55 | let mut world = get_world_with_hands(&vulkan_context, &mut render_context); 56 | 57 | skinning_system_inner(&mut world, &mut render_context); 58 | 59 | assert!(verify_matrices(&world, &render_context)); 60 | 61 | // Muck all the joints up 62 | for (_, skin) in world.query::<&Skin>().iter() { 63 | for joint in &skin.joints { 64 | let mut global_transform = world.get::<&mut GlobalTransform>(*joint).unwrap(); 65 | global_transform.0 = Affine3A::ZERO; 66 | } 67 | } 68 | skinning_system_inner(&mut world, &mut render_context); 69 | 70 | // TODO: This test is broken: https://github.com/leetvr/hotham/issues/370 71 | // assert!(verify_matrices(&world, &render_context)); 72 | } 73 | 74 | fn verify_matrices(world: &World, render_context: &RenderContext) -> bool { 75 | let mut called = 0; 76 | for (_, (skin, info)) in world.query::<(&Skin, &Info)>().iter() { 77 | let correct_matrices: Vec = if info.name == "hands:Lhand" { 78 | serde_json::from_slice(include_bytes!( 79 | "../../../test_assets/left_hand_skinned_matrices.json" 80 | )) 81 | .unwrap() 82 | } else { 83 | serde_json::from_slice(include_bytes!( 84 | "../../../test_assets/right_hand_skinned_matrices.json" 85 | )) 86 | .unwrap() 87 | }; 88 | let buffer = unsafe { render_context.resources.skins_buffer.as_slice() }; 89 | let joint_matrices = &buffer[skin.id as usize]; 90 | 91 | for i in 0..correct_matrices.len() { 92 | let expected = correct_matrices[i]; 93 | let actual = joint_matrices[i]; 94 | if !relative_eq!(expected, actual) { 95 | println!("Matrix {i} is incorrect"); 96 | println!("Actual:"); 97 | println!("{}", serde_json::to_string_pretty(&actual).unwrap()); 98 | println!("Expected:"); 99 | println!("{}", serde_json::to_string_pretty(&expected).unwrap()); 100 | std::fs::File::create("matrix_failed.json") 101 | .unwrap() 102 | .write_all(&serde_json::to_vec_pretty(&joint_matrices[..]).unwrap()) 103 | .unwrap(); 104 | return false; 105 | } 106 | } 107 | called += 1; 108 | } 109 | assert_ne!(called, 0); 110 | 111 | true 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /hotham/src/workers/mod.rs: -------------------------------------------------------------------------------- 1 | use hotham_asset_client::{watch, AssetUpdatedMessage}; 2 | 3 | use std::sync::mpsc; 4 | 5 | #[derive(Debug, Clone)] 6 | pub(crate) enum WorkerMessage { 7 | AssetUpdated(AssetUpdatedMessage), 8 | Error(WorkerError), 9 | } 10 | 11 | #[derive(Debug, Clone)] 12 | pub(crate) enum WorkerError { 13 | TaskFailed(String), 14 | } 15 | 16 | pub(crate) struct Workers { 17 | pub(crate) receiver: mpsc::Receiver, 18 | } 19 | 20 | impl Workers { 21 | pub fn new(asset_list: Vec) -> Self { 22 | let (to_engine, from_worker) = mpsc::channel(); 23 | if asset_list.is_empty() { 24 | return Self { 25 | receiver: from_worker, 26 | }; 27 | } 28 | 29 | std::thread::spawn(|| { 30 | let local_set = tokio::task::LocalSet::new(); 31 | let (to_workers, mut from_asset_watcher) = tokio::sync::mpsc::channel(100); 32 | let to_engine_1 = to_engine.clone(); 33 | local_set.spawn_local(async move { 34 | watch(asset_list, to_workers).await.map_err(|e| { 35 | to_engine_1.send(WorkerMessage::Error(WorkerError::TaskFailed(format!( 36 | "{e:?}" 37 | )))) 38 | }) 39 | }); 40 | local_set.spawn_local(async move { 41 | loop { 42 | if let Some(message) = from_asset_watcher.recv().await { 43 | to_engine 44 | .send(WorkerMessage::AssetUpdated(message)) 45 | .unwrap(); 46 | } 47 | } 48 | }); 49 | 50 | let runtime = tokio::runtime::Builder::new_current_thread() 51 | .enable_all() 52 | .build() 53 | .unwrap(); 54 | 55 | println!("[HOTHAM_WORKER] Runtime starting.."); 56 | runtime.block_on(local_set); 57 | panic!("[HOTHAM_WORKER] RUNTIME FINISHED - THIS SHOULD NOT HAPPEN"); 58 | }); 59 | 60 | Self { 61 | receiver: from_worker, 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/logo.jpg -------------------------------------------------------------------------------- /openxr_loader.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/openxr_loader.dll -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | # based on https://nixos.org/manual/nixpkgs/stable/#android 4 | # note: if you get any weird errors of "required extensions not found" or the like, somehow hotham 5 | # doesn't search /run/opengl-driver 6 | # you can set VK_ICD_FILENAMES manually instead, see https://nixos.org/manual/nixos/stable/index.html#sec-gpu-accel-vulkan 7 | let 8 | cmakeVersion = "3.22.1"; 9 | 10 | androidComposition = pkgs.androidenv.composeAndroidPackages { 11 | includeNDK = true; 12 | ndkVersion = "22.1.7171670"; 13 | platformVersions = ["28"]; 14 | cmakeVersions = [cmakeVersion]; 15 | }; 16 | 17 | openXrDropin = { 18 | file_format_version = "1.0.0"; 19 | runtime = { 20 | api_version = "1.0"; 21 | name = "Hotham Simulator"; 22 | # this won't work with pure flakes but flakes are unstable anyway 23 | library_path = "${toString ./.}/target/debug/libhotham_simulator.so"; 24 | }; 25 | }; 26 | in 27 | pkgs.mkShell rec { 28 | buildInputs = with pkgs; [ 29 | rustup ninja cmake 30 | openssl pkg-config 31 | 32 | cargo-apk adoptopenjdk-bin 33 | 34 | shaderc python3 35 | vulkan-headers vulkan-loader 36 | vulkan-tools vulkan-tools-lunarg 37 | vulkan-validation-layers vulkan-extension-layer 38 | monado openxr-loader openxr-loader.dev 39 | 40 | libxkbcommon 41 | wayland xorg.libX11 xorg.libXcursor xorg.libXrandr xorg.libXi 42 | fontconfig freetype 43 | alsa-lib 44 | 45 | renderdoc 46 | ]; 47 | 48 | # the nixpkgs manual lists that ANDROID_HOME is outdated and ANDROID_SDK_ROOT should be used instead 49 | # when actually running the android tools, they say the opposite, ANDROID_SDK_ROOT being outdated 50 | # so I just set both 51 | ANDROID_SDK_ROOT = "${androidComposition.androidsdk}/libexec/android-sdk"; 52 | ANDROID_HOME = ANDROID_SDK_ROOT; 53 | ANDROID_NDK_ROOT = "${ANDROID_SDK_ROOT}/ndk-bundle"; 54 | 55 | shellHook = '' 56 | export PATH="$(echo "$ANDROID_SDK_ROOT/cmake/${cmakeVersion}".*/bin):$PATH" 57 | export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${builtins.toString (pkgs.lib.makeLibraryPath buildInputs)}"; 58 | export XR_RUNTIME_JSON="${builtins.toFile "hotham-openxr-runtime.json" (builtins.toJSON openXrDropin)}" 59 | 60 | rustup default 1.67 # required due to libunwind having fun 61 | rustup target add aarch64-linux-android 62 | ''; 63 | } 64 | -------------------------------------------------------------------------------- /test_assets/Quartet 14 - Clip.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e4866b9e24ea6bba6c764f434460bfd5a19718742384e276249d6f0305470bef 3 | size 2368036 4 | -------------------------------------------------------------------------------- /test_assets/asteroid.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a29081bb2bed055a062ac64b41df14948908738cda6f2d3abb9bbde148d96baf 3 | size 23616344 4 | -------------------------------------------------------------------------------- /test_assets/box_no_material.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:664d4544f9c287f76abe0408fc456d043f41c613fbacae795882ec7388dbe928 3 | size 1740 4 | -------------------------------------------------------------------------------- /test_assets/box_with_colliders.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2cd373968e7cf532923f1be43851c7b01b41925d5ed96183a6b6e140168595de 3 | size 31240 4 | -------------------------------------------------------------------------------- /test_assets/cube.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:aaabc33da12631937d1fa586dbfb9a3fa0cdfc256160e52384733c80d43215f3 3 | size 2044 4 | -------------------------------------------------------------------------------- /test_assets/culling_stress_test.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9a154196db8e1ad47a50d45fc6cc53f9d1a91b3a70ad682e37bd264a49465719 3 | size 15290044 4 | -------------------------------------------------------------------------------- /test_assets/damaged_helmet.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5922d73673fa4a2e14e3622a543ccdadef464ab261f17416455d5209db6d7839 3 | size 16598244 4 | -------------------------------------------------------------------------------- /test_assets/damaged_helmet_squished.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:76d6c5f831088a132b01426b83e177569feb2c7a000d34d689c538b96cc5dc81 3 | size 16117496 4 | -------------------------------------------------------------------------------- /test_assets/ferris-the-crab/source/ferris.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ba3a61f50ac3d5c17a63b817f7ea65d687f45445e3dd492f3d0bb75a846534bf 3 | size 221636 4 | -------------------------------------------------------------------------------- /test_assets/ferris-the-crab/textures/ferris_eyes_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetvr/hotham/cab13bfcc2dcce1828d0520c03950a28668f5330/test_assets/ferris-the-crab/textures/ferris_eyes_0.png -------------------------------------------------------------------------------- /test_assets/floor.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:98eb2cde11da5a7a4490cde5a004f5d1d8fddc30b4e5a2cbff52249eda5a1529 3 | size 42276 4 | -------------------------------------------------------------------------------- /test_assets/gymnopedie.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:857dde693fae9e4e50a51fbe4fa4b202ae1468d39b054d2f2e58ab4660b7c793 3 | size 6929028 4 | -------------------------------------------------------------------------------- /test_assets/hologram_templates.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7b1766b1019fc57d9da019328b8ea9929b03082c4aae6dc216dbed4fe6cf2f12 3 | size 14236 4 | -------------------------------------------------------------------------------- /test_assets/ibl_test.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d00cc3d91bc5c3b1afef9fbbe62574c73036facf50ad72c1cd9085d92e863095 3 | size 361720 4 | -------------------------------------------------------------------------------- /test_assets/ibl_test_squished.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9e88e7e256978f873a193e787e48a8cf5512a6efe3f7e240c56bd56fbb7f1a7c 3 | size 476624 4 | -------------------------------------------------------------------------------- /test_assets/ice_crash.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ff4e9f9c351c1ef48e92863e111b690d8c8ad8a47ba3b13171116a41161fa8bf 3 | size 39775 4 | -------------------------------------------------------------------------------- /test_assets/left_hand.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:33e57bcf816fe8b70250ca54b603f390e36b630ee0afe543ca66ca60f1344b3b 3 | size 95132 4 | -------------------------------------------------------------------------------- /test_assets/normal_tangent_mirror_test.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:705465c5045c113c680be8631e9295db8fe8e6342b0e5f9b094b89d33d9bb12a 3 | size 2019856 4 | -------------------------------------------------------------------------------- /test_assets/normal_tangent_test.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7b8f8bf00c5fa43fc97a3e36aadb9eed949f08622785695aaf9c674090d9f1ae 3 | size 1921368 4 | -------------------------------------------------------------------------------- /test_assets/render_Diffuse.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:283cb92296bc937a429b808b16746cce4012460824aa3842579e0cbf5b26dc2f 3 | size 45682 4 | -------------------------------------------------------------------------------- /test_assets/render_Diffuse_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:283cb92296bc937a429b808b16746cce4012460824aa3842579e0cbf5b26dc2f 3 | size 45682 4 | -------------------------------------------------------------------------------- /test_assets/render_Full.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5266b2a6775edb3c79ca9067a81eb58288a08cf6ead6862292f3e2b7634204d6 3 | size 50094 4 | -------------------------------------------------------------------------------- /test_assets/render_Full_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5266b2a6775edb3c79ca9067a81eb58288a08cf6ead6862292f3e2b7634204d6 3 | size 50094 4 | -------------------------------------------------------------------------------- /test_assets/render_No_IBL.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:537f33e06705dbfa2159c66d171ad75a10aaeec5e0b072f55e3cb9d04eab5264 3 | size 24097 4 | -------------------------------------------------------------------------------- /test_assets/render_No_IBL_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:537f33e06705dbfa2159c66d171ad75a10aaeec5e0b072f55e3cb9d04eab5264 3 | size 24097 4 | -------------------------------------------------------------------------------- /test_assets/render_Normals.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:926e260e819609d1b23ff591b8c5e65007a8f30d2020064ee1e0f9c181845040 3 | size 41878 4 | -------------------------------------------------------------------------------- /test_assets/render_Normals_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:926e260e819609d1b23ff591b8c5e65007a8f30d2020064ee1e0f9c181845040 3 | size 41878 4 | -------------------------------------------------------------------------------- /test_assets/render_Spotlight.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4f866483aa0d13a13d3cd2afbda55a5c81d8c67a3b3f46996284fef2aad0b9d8 3 | size 31373 4 | -------------------------------------------------------------------------------- /test_assets/render_Spotlight_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4f866483aa0d13a13d3cd2afbda55a5c81d8c67a3b3f46996284fef2aad0b9d8 3 | size 31373 4 | -------------------------------------------------------------------------------- /test_assets/render_draw_gui.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:de5cde4144b368492f0e3fd78f0570329a885adacf1a074c919c403c0f1538db 3 | size 23771 4 | -------------------------------------------------------------------------------- /test_assets/render_draw_gui_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:de5cde4144b368492f0e3fd78f0570329a885adacf1a074c919c403c0f1538db 3 | size 23771 4 | -------------------------------------------------------------------------------- /test_assets/render_normal_tangent_test_IBL.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5fca86cf9d80d007c05bbd1a1bd0689296b170e6c467c6aa4680b28af88323da 3 | size 72085 4 | -------------------------------------------------------------------------------- /test_assets/render_normal_tangent_test_IBL_and_spotlight.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:523cc42bcf0f5ddebf6527b6f9734e2365027ff66a4c7ccf732a5077468fceca 3 | size 74460 4 | -------------------------------------------------------------------------------- /test_assets/render_normal_tangent_test_IBL_and_spotlight_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:523cc42bcf0f5ddebf6527b6f9734e2365027ff66a4c7ccf732a5077468fceca 3 | size 74460 4 | -------------------------------------------------------------------------------- /test_assets/render_normal_tangent_test_IBL_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5fca86cf9d80d007c05bbd1a1bd0689296b170e6c467c6aa4680b28af88323da 3 | size 72085 4 | -------------------------------------------------------------------------------- /test_assets/render_normal_tangent_test_Normals.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6b5e45ccc5794d18e73942bbdd6a668e8a0b32f4bc3424e9c60e184dd1a088f7 3 | size 71646 4 | -------------------------------------------------------------------------------- /test_assets/render_normal_tangent_test_Normals_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6b5e45ccc5794d18e73942bbdd6a668e8a0b32f4bc3424e9c60e184dd1a088f7 3 | size 71646 4 | -------------------------------------------------------------------------------- /test_assets/render_normal_tangent_test_Spotlight1.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a17ac6e324cd260e2917470feb0c1b5ac0aaf3ce3123d2af5e0b5be6d488ffa5 3 | size 48865 4 | -------------------------------------------------------------------------------- /test_assets/render_normal_tangent_test_Spotlight1_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a17ac6e324cd260e2917470feb0c1b5ac0aaf3ce3123d2af5e0b5be6d488ffa5 3 | size 48865 4 | -------------------------------------------------------------------------------- /test_assets/render_normal_tangent_test_Spotlight2.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:382a73d6ea34b99868883360243435b27501ace0ddf531a9d81a3e428d690cbe 3 | size 53614 4 | -------------------------------------------------------------------------------- /test_assets/render_normal_tangent_test_Spotlight2_known_good.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:382a73d6ea34b99868883360243435b27501ace0ddf531a9d81a3e428d690cbe 3 | size 53614 4 | -------------------------------------------------------------------------------- /test_assets/renderdoc_performance_counters.json: -------------------------------------------------------------------------------- 1 | { 2 | "counters": [ 3 | [ 4 | 1, 5 | 0, 6 | 0, 7 | 0 8 | ], 9 | [ 10 | 3, 11 | 0, 12 | 0, 13 | 0 14 | ], 15 | [ 16 | 4, 17 | 0, 18 | 0, 19 | 0 20 | ], 21 | [ 22 | 5, 23 | 0, 24 | 0, 25 | 0 26 | ], 27 | [ 28 | 6, 29 | 0, 30 | 0, 31 | 0 32 | ], 33 | [ 34 | 7, 35 | 0, 36 | 0, 37 | 0 38 | ], 39 | [ 40 | 15, 41 | 0, 42 | 0, 43 | 0 44 | ], 45 | [ 46 | 16, 47 | 0, 48 | 0, 49 | 0 50 | ], 51 | [ 52 | 21, 53 | 0, 54 | 0, 55 | 0 56 | ], 57 | [ 58 | 22, 59 | 0, 60 | 0, 61 | 0 62 | ], 63 | [ 64 | 23, 65 | 0, 66 | 0, 67 | 0 68 | ], 69 | [ 70 | 27, 71 | 0, 72 | 0, 73 | 0 74 | ], 75 | [ 76 | 28, 77 | 0, 78 | 0, 79 | 0 80 | ], 81 | [ 82 | 30, 83 | 0, 84 | 0, 85 | 0 86 | ], 87 | [ 88 | 31, 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | [ 94 | 33, 95 | 0, 96 | 0, 97 | 0 98 | ], 99 | [ 100 | 34, 101 | 0, 102 | 0, 103 | 0 104 | ], 105 | [ 106 | 35, 107 | 0, 108 | 0, 109 | 0 110 | ], 111 | [ 112 | 40, 113 | 0, 114 | 0, 115 | 0 116 | ], 117 | [ 118 | 41, 119 | 0, 120 | 0, 121 | 0 122 | ], 123 | [ 124 | 42, 125 | 0, 126 | 0, 127 | 0 128 | ], 129 | [ 130 | 43, 131 | 0, 132 | 0, 133 | 0 134 | ], 135 | [ 136 | 45, 137 | 0, 138 | 0, 139 | 0 140 | ], 141 | [ 142 | 46, 143 | 0, 144 | 0, 145 | 0 146 | ] 147 | ], 148 | "rdocPerformanceCounterSettings": 1 149 | } 150 | -------------------------------------------------------------------------------- /test_assets/right_hand.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:92f636788d02bf908f96b04182aa9072f2d7f7718410dcc5b8928fd6e2b4e49b 3 | size 95332 4 | -------------------------------------------------------------------------------- /test_assets/right_here_beside_you_clip.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6a888bcbcc5ab0d2d82932697b35243199327ec7721e71591ba8a0660e806bea 3 | size 6978908 4 | -------------------------------------------------------------------------------- /test_assets/sphere.glb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f25ae974673d67512913c98b0d81095c6a1cf9237d4a00662b3adb3e20c0a3a2 3 | size 1960 4 | -------------------------------------------------------------------------------- /test_assets/tell_me_that_i_cant_clip.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ce668126834a4b9e9d790b5418e8f2534769df404559eb23dd2feafea965cd53 3 | size 439937 4 | --------------------------------------------------------------------------------