mirror of
https://github.com/simplysanatori/LegendFrame.git
synced 2026-10-29 03:02:07 +01:00
1.1.0: OpenXR with OpenVR fallback, settings panel, god mode, body physics
- OpenXR by default (openxr_loader.dll, Khronos 1.1.63), falling back to OpenVR / SteamVR; [vr] vr_runtime = auto|openxr|openvr. OpenXR grip poses converted to the OpenVR grip so the hands sit the same under both. - Settings panel in the headset (both sticks held, or the Touch menu button), worked with a main-hand pointer; TR7-Menu-Hook's options built in when it is installed. - God mode pick-up, carry and throw; third-person chest and rear physics; body push. - Smooth view transitions; picture brightness and contrast; 4:3 movies; version and runtime on the main menu. - Palm sliders: third "side" slider, per-preset scale/zoom/side, notches at the defaults, index-fingertip grab from Lara's drawn finger. - Per-model caching of the body measurements (stutter with some player model mods). - Docs, release notes and notices for 1.1.0.
This commit is contained in:
1 parent
2e745fb89c
commit
e3657d0fe5
118 files changed
+102898
-6874
No files matched your search
@@ -4,7 +4,8 @@ Copyright 2026 Rototrobot
|
||||
Licensed under the Apache License, Version 2.0 (see LICENSE).
|
||||
|
||||
This repository also contains third-party code under its own licences
|
||||
(DXVK: zlib/libpng; OpenVR: BSD 3-Clause; TRAWindowed: MIT). See
|
||||
(DXVK: zlib/libpng; OpenVR: BSD 3-Clause; OpenXR SDK: Apache 2.0;
|
||||
Dear ImGui: MIT; TRAWindowed: MIT). See
|
||||
THIRD_PARTY_NOTICES.md.
|
||||
|
||||
Tomb Raider: Legend is a trademark of its owners. This project is an
|
||||
|
||||
@@ -6,7 +6,9 @@ VR third-person camera, and an optional first-person mode where you climb,
|
||||
shoot and swing with your own hands.
|
||||
|
||||
It runs as a Direct3D 9 proxy (`d3d9.dll`) beside a small fork of DXVK, with a
|
||||
launcher that makes the game large-address-aware.
|
||||
launcher that makes the game large-address-aware. It talks to the headset
|
||||
through OpenXR, and falls back to OpenVR (SteamVR) when OpenXR is not
|
||||
available.
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
@@ -15,9 +17,10 @@ launcher that makes the game large-address-aware.
|
||||
> about that.
|
||||
|
||||
> [!NOTE]
|
||||
> **Version 1.0.0.** It is playable from start to finish in third person and
|
||||
> **Version 1.1.0.** It is playable from start to finish in third person and
|
||||
> most of the way in first person. Bug reports with logs are very welcome; see
|
||||
> [Troubleshooting](#troubleshooting).
|
||||
> [Troubleshooting](#troubleshooting). What changed is in
|
||||
> [`release/RELEASE_NOTES_1.1.0.md`](release/RELEASE_NOTES_1.1.0.md).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Windows may block this mod from running.**
|
||||
@@ -49,8 +52,9 @@ her shoulder.
|
||||
## Requirements
|
||||
|
||||
- **Tomb Raider: Legend** for PC: the Steam or the GOG release
|
||||
- A SteamVR-compatible headset with two tracked motion controllers
|
||||
- **SteamVR** installed and running
|
||||
- A PC VR headset with two tracked motion controllers
|
||||
- An **OpenXR runtime** that runs 32-bit games (SteamVR's does), or **SteamVR**
|
||||
for the OpenVR fallback
|
||||
- A GPU with Vulkan support (DXVK translates the game's Direct3D 9 to Vulkan)
|
||||
- Windows
|
||||
|
||||
@@ -59,6 +63,11 @@ Valve Index, WMR and Reverb G2 controllers. Nothing is specific to one headset:
|
||||
field of view, IPD and render resolution are all read from the runtime at
|
||||
start-up.
|
||||
|
||||
**OpenXR or OpenVR.** `vr_runtime = auto` (the default) uses OpenXR and falls
|
||||
back to OpenVR / SteamVR if OpenXR cannot start. Delete `openxr_loader.dll`, or
|
||||
set `vr_runtime = openvr`, to always use OpenVR. The hands sit in the same
|
||||
place under both. The main menu shows the version and the runtime in use.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
@@ -76,8 +85,12 @@ start-up.
|
||||
and nothing behind you is drawn for nothing.
|
||||
- **Level horizon.** The game camera's pitch and roll stay out of your view;
|
||||
only your head moves it.
|
||||
- **A stable compositor hand-off.** The image given to SteamVR is pinned in
|
||||
video memory (the lesson from the SiN VR mod's NVIDIA crash).
|
||||
- **A stable compositor hand-off.** The image given to the headset is pinned
|
||||
in video memory (the lesson from the SiN VR mod's NVIDIA crash).
|
||||
- **Picture controls**: brightness and contrast for the headset image, which
|
||||
follow the game's own brightness setting by default.
|
||||
- **Movies at their real shape**: the 4:3 movies are no longer squeezed on the
|
||||
movie screen.
|
||||
|
||||
|
||||
### Third person (default)
|
||||
@@ -86,9 +99,19 @@ start-up.
|
||||
auto-centring or shake), `shoulder` (close behind Lara at life size) and
|
||||
`board` (Lara a few inches tall on a tabletop world).
|
||||
- **Camera collision** keeps the view out of rock and walls.
|
||||
- **Scale and zoom by hand**: turn your off hand palm up to bring out two
|
||||
sliders that resize the world and move the camera in or out, saved per
|
||||
camera style. Lara's VR hands scale with it.
|
||||
- **Smooth view changes**: switching between the camera styles and first
|
||||
person glides instead of cutting (`view_transition_seconds`).
|
||||
- **Scale, zoom and side by hand**: turn your off hand palm up to bring out
|
||||
three sliders: the size of the world, the camera nearer or farther, and the
|
||||
camera to the left or right of Lara (over either shoulder). Touch a knob
|
||||
with your other palm or index finger, grip and slide. Each camera style
|
||||
keeps its own; a small notch marks each default, and the knob sticks there
|
||||
with a click. Lara's VR hands scale with it.
|
||||
- **God mode** (`board`): pinch or make a fist on Lara to pick her up, carry
|
||||
her, and throw her by letting go mid-swing. She falls and lands with the
|
||||
game's own physics. A hand leaning on her pushes her body away.
|
||||
- **Body physics**: her chest and rear give and bounce as she moves and when
|
||||
your hands touch them.
|
||||
- **Board diorama** (`board_diorama = 1`): in `board` the tabletop world sits
|
||||
in an open box in a dark room; grab the bar on its edge to move it.
|
||||
- **Theatre mode**: cutscenes, and optionally third-person play, are shown
|
||||
@@ -155,6 +178,12 @@ start-up.
|
||||
button instead of the keyboard key, including for left-handed play.
|
||||
- **Smooth or snap turning**, an optional comfort vignette, and movies and
|
||||
loading screens shown on a world-locked screen.
|
||||
- **Settings panel in the headset**: hold both sticks in for 0.6 s (Oculus
|
||||
Touch: hold the left menu button) or press Insert on the keyboard. It floats
|
||||
in front of you; point at it with your main hand and pull the trigger, or use
|
||||
the sticks and A / B. Every change applies at once and is saved to
|
||||
`trlvr.ini`: cameras, picture, body physics, god mode, theatre, diorama and
|
||||
controls.
|
||||
- **VR settings in the pause menu** (immersive controls, during a level):
|
||||
*Display* becomes *VR Settings*, and its entries change VR options on the spot
|
||||
and save them to `trlvr.ini`. Press an entry to step through its choices:
|
||||
@@ -174,11 +203,12 @@ start-up.
|
||||
is what counts.
|
||||
- **Five languages**: the mod's own text follows the game's language (English,
|
||||
French, German, Italian, Spanish).
|
||||
- **Other mods' menus**: TR7-Menu-Hook's menu (the loader the Lara Remastered
|
||||
Player Model Mod needs) is shown on a panel over your off hand. Point your
|
||||
main hand at it and pull the main trigger to click; while you point at it the
|
||||
game does not see that trigger. `imgui_panel = 0` turns it off. Player model
|
||||
mods with many more bones work in first person.
|
||||
- **TR7-Menu-Hook built in**: with TR7-Menu-Hook installed (the loader the Lara
|
||||
Remastered Player Model Mod needs), its options appear in the settings panel
|
||||
as an ordinary section: refill health, next outfit, game speed, wireframe,
|
||||
hide UI, the debug keypad, and its saved settings. "Open its full menu"
|
||||
still shows its own window. Player model mods with many more bones work in
|
||||
first person and third person.
|
||||
|
||||
---
|
||||
|
||||
@@ -212,7 +242,8 @@ C:\Program Files (x86)\Steam\steamapps\common\Tomb Raider Legend\
|
||||
|---|---|
|
||||
| `d3d9.dll` | The mod (a Direct3D 9 proxy the game loads) |
|
||||
| `d3d9_dxvk.dll` | The DXVK fork that renders through Vulkan |
|
||||
| `openvr_api.dll` | OpenVR, 32-bit |
|
||||
| `openxr_loader.dll` | OpenXR, 32-bit (the default runtime; delete it to use OpenVR) |
|
||||
| `openvr_api.dll` | OpenVR, 32-bit (the fallback) |
|
||||
| `trlvr_launcher.exe` | The launcher (see below) |
|
||||
| `trlvr_actions.json`, `bindings_*.json` | The SteamVR action manifest and controller bindings |
|
||||
|
||||
@@ -232,7 +263,8 @@ C:\Program Files (x86)\Steam\steamapps\common\Tomb Raider Legend\
|
||||
|
||||
Adjust the path if your library is elsewhere, then launch from Steam as usual.
|
||||
|
||||
**GOG:** start SteamVR, then run `trlvr_launcher.exe` from the game folder.
|
||||
**GOG:** start your VR runtime (SteamVR), then run `trlvr_launcher.exe` from
|
||||
the game folder.
|
||||
|
||||
#### Why a launcher?
|
||||
|
||||
@@ -255,7 +287,8 @@ Touch-style controllers (Reverb G2, Quest, Rift):
|
||||
| Control | Action |
|
||||
|---|---|
|
||||
| Left stick | Move |
|
||||
| Left stick click / double click / hold | Gear menu / recentre the view / pause |
|
||||
| Left stick click / double click / hold | Gear menu / recentre the view / pause (Oculus Touch: the left menu button) |
|
||||
| Both sticks held in | Settings panel (Oculus Touch: hold the left menu button) |
|
||||
| Right stick | Turn |
|
||||
| Right stick click / double click / hold | Switch weapons / PDA / binoculars |
|
||||
| Left trigger | Action, interact, pull the grapple |
|
||||
@@ -378,6 +411,13 @@ first and third person at any time in gameplay.
|
||||
| `terrain_culling` | `frustum` | `open` draws every terrain strip of a visible room |
|
||||
| `depth_of_field` | `off` | `on` or `game` (keep the launcher's setting) |
|
||||
| `world_scale` | `291` | Game units per metre. Raise to feel smaller, lower to feel larger |
|
||||
| `vr_runtime` | `auto` | `openxr`, `openvr`, or `auto` (OpenXR, falling back to OpenVR) |
|
||||
| `picture_brightness` / `picture_contrast` | `0` / `1` | The headset image; `picture_follow_game = 1` also follows the game's brightness |
|
||||
| `view_transition_seconds` | `0.3` | How long switching camera style glides (`0` cuts) |
|
||||
| `body_rear` | `1` | Third person: her rear gives and bounces |
|
||||
| `body_push` | `1` | God mode: a hand leaning on her pushes her body away |
|
||||
| `board_god_hand` | `1` | `board`: pick Lara up, carry and throw her |
|
||||
| `god_throw` | `1` | Letting go mid-swing throws her (`god_throw_strength`) |
|
||||
|
||||
### Performance
|
||||
|
||||
@@ -407,7 +447,10 @@ and the costliest objects. It says why an area is slow.
|
||||
- The **Vulkan SDK** installed under `C:\VulkanSDK` (headers and
|
||||
`glslangValidator`).
|
||||
- **Python 3** with **meson** and **ninja**: `py -3 -m pip install meson ninja`.
|
||||
- OpenVR is included in [`third_party/openvr`](third_party/openvr).
|
||||
- OpenXR (headers and loader) is included in
|
||||
[`third_party/openxr`](third_party/openxr), OpenVR in
|
||||
[`third_party/openvr`](third_party/openvr) and Dear ImGui in
|
||||
[`third_party/imgui`](third_party/imgui).
|
||||
|
||||
Everything is built **32-bit**: `trl.exe` is a 32-bit process.
|
||||
|
||||
@@ -430,7 +473,7 @@ package.bat :: the drag-and-drop release in %USERPROFIL
|
||||
```
|
||||
|
||||
The DXVK fork (`dxvk-trlvr/`) adds `Direct3DCreateVR9` (`src/d3d9/d3d9_vr.*`),
|
||||
which hands the Vulkan image behind a Direct3D 9 surface to OpenVR, and a DXVK
|
||||
which hands the Vulkan image behind a Direct3D 9 surface to the VR runtime, and a DXVK
|
||||
profile for `trl_vr.exe`. The shipped `d3d9_dxvk.dll` is a **modified** DXVK
|
||||
build; report problems with it here, not upstream.
|
||||
|
||||
@@ -442,11 +485,15 @@ build; report problems with it here, not upstream.
|
||||
| `src/vr` | VR session and submit, cameras and first person, culling, input and gestures, UI, water |
|
||||
| `src/common` | `trlvr.ini` settings and the log |
|
||||
| `src/launcher` | `trlvr_launcher.exe` |
|
||||
| `src/vr/physics` | Body physics: soft chest, rear, body push and volumes |
|
||||
| `src/vr/godmode` | God mode: pick-up, carry and throw |
|
||||
| `src/test` | Maths and proxy tests |
|
||||
| `input` | SteamVR action manifest and controller bindings |
|
||||
| `release` | Player documentation shipped in the package |
|
||||
| `dxvk-trlvr` | DXVK fork |
|
||||
| `third_party/openxr` | OpenXR headers and loader |
|
||||
| `third_party/openvr` | OpenVR header and runtime |
|
||||
| `third_party/imgui` | Dear ImGui, for the settings panel |
|
||||
|
||||
---
|
||||
|
||||
@@ -472,7 +519,7 @@ build; report problems with it here, not upstream.
|
||||
Clear the Steam launch option, then delete from the game folder:
|
||||
|
||||
```
|
||||
d3d9.dll d3d9_dxvk.dll openvr_api.dll trlvr_launcher.exe
|
||||
d3d9.dll d3d9_dxvk.dll openxr_loader.dll openvr_api.dll trlvr_launcher.exe
|
||||
trlvr_actions.json bindings_*.json
|
||||
trlvr.ini trlvr.log* trlvr_crash.dmp* trl_vr.exe
|
||||
trlvr_launcher.log trl_vr_d3d9.log
|
||||
@@ -490,15 +537,17 @@ in the Setup dialog afterwards; set them back there if you want.
|
||||
**The game runs on the monitor but nothing appears in the headset.**
|
||||
Check `trlvr.log` beside the game. It records the headset, every hook and the
|
||||
controllers, and says plainly when something fails. Make sure the game was
|
||||
started through `trlvr_launcher.exe` and that SteamVR was running.
|
||||
started through `trlvr_launcher.exe` and that your VR runtime was running.
|
||||
If OpenXR is the problem, try OpenVR: `vr_runtime = openvr` in `trlvr.ini`.
|
||||
|
||||
**The view is offset or facing the wrong way.**
|
||||
Double-click the left stick (or press F1) to recentre.
|
||||
|
||||
**Controller buttons do nothing, or the wrong thing.**
|
||||
SteamVR may have kept an older or custom binding. Open SteamVR → Settings →
|
||||
Controllers → Manage Controller Bindings and select the supplied Tomb Raider:
|
||||
Legend VR binding.
|
||||
Under OpenVR, SteamVR may have kept an older or custom binding. Open SteamVR →
|
||||
Settings → Controllers → Manage Controller Bindings and select the supplied
|
||||
Tomb Raider: Legend VR binding. Under OpenXR the bindings are built into the
|
||||
mod.
|
||||
|
||||
**Performance is poor.**
|
||||
See [Performance](#performance). The `perf:` lines in `trlvr.log` show whether
|
||||
@@ -508,7 +557,7 @@ it is the GPU or the CPU and which objects cost the most.
|
||||
Send the files beside the game: `trlvr.log` and `trlvr_crash.dmp`, plus
|
||||
`trl_vr_d3d9.log`. If you have started the game again since, the previous run
|
||||
is kept as `trlvr.log.prev` and `trlvr_crash.dmp.prev`. The crash report names
|
||||
the module and the SteamVR or DXVK call in progress, and the dump holds the
|
||||
the module and the VR runtime or DXVK call in progress, and the dump holds the
|
||||
rest.
|
||||
|
||||
---
|
||||
@@ -524,6 +573,9 @@ Not covered by it, see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md):
|
||||
- **DXVK** (zlib/libpng). The release's `d3d9_dxvk.dll` is a **modified** build.
|
||||
- **OpenVR SDK** (BSD 3-Clause, © Valve). `openvr_api.dll` is redistributed
|
||||
unmodified.
|
||||
- **OpenXR SDK** (Apache 2.0, © The Khronos Group). `openxr_loader.dll` is
|
||||
built from the unmodified source.
|
||||
- **Dear ImGui** (MIT, © Omar Cornut), for the settings panel.
|
||||
- **TRAWindowed** (MIT, © chreden), which the D3D9 proxy started from.
|
||||
|
||||
Tomb Raider: Legend is © its owners. This mod is unofficial, not affiliated
|
||||
@@ -543,6 +595,8 @@ game data, and needs a legitimate copy of the game.
|
||||
> compositor at all.
|
||||
> - **chreden**, for **TRAWindowed**, the D3D9 wrapper this mod's proxy grew from.
|
||||
> - **Valve**, for **OpenVR** and SteamVR.
|
||||
> - **The Khronos Group**, for **OpenXR**.
|
||||
> - **Omar Cornut**, for **Dear ImGui**.
|
||||
> - **Crystal Dynamics**, for making Tomb Raider: Legend.
|
||||
|
||||
Any mistakes in this mod are my own, not theirs.
|
||||
|
||||
+17
-1
@@ -12,7 +12,23 @@ dxbc-spirv, libdisplay-info) keep their own licences in their folders.
|
||||
|
||||
`openvr.h` and the 32-bit `openvr_api.dll` from Valve's
|
||||
[OpenVR SDK](https://github.com/ValveSoftware/openvr).
|
||||
BSD 3-Clause licence; see `third_party/openvr/LICENSE`.
|
||||
BSD 3-Clause licence; see `third_party/openvr/LICENSE`. Used when OpenXR is
|
||||
not available.
|
||||
|
||||
## OpenXR SDK (`third_party/openxr/`)
|
||||
|
||||
The OpenXR headers and the 32-bit OpenXR loader (`openxr_loader.dll`, built
|
||||
unmodified) from the Khronos
|
||||
[OpenXR-SDK-Source](https://github.com/KhronosGroup/OpenXR-SDK-Source),
|
||||
release 1.1.63. Apache 2.0 licence (the loader bundles jsoncpp, MIT / public
|
||||
domain); see `third_party/openxr/LICENSE` and `third_party/openxr/COPYING.adoc`.
|
||||
`third_party/openxr/README.md` says how it was built.
|
||||
|
||||
## Dear ImGui (`third_party/imgui/`)
|
||||
|
||||
[Dear ImGui](https://github.com/ocornut/imgui) 1.91.8 by Omar Cornut, with
|
||||
its Direct3D 9 and Win32 backends, unmodified. Draws the in-game settings
|
||||
panel. MIT licence; see `third_party/imgui/LICENSE.txt`.
|
||||
|
||||
## TRAWindowed
|
||||
|
||||
|
||||
@@ -24,10 +24,16 @@ set SRC=%SRC% "%ROOT%src\proxy\imgui_overlay.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\proxy\proxy_swapchain.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\proxy\window.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\proxy\gpu_profile.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\proxy\overlay.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\common\log.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\common\config.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\vr_math.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\vr_session.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\xr_runtime.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\ovr_runtime.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\vr_runtime.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\menu_watermark.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\tr7_menu_hook.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\vr_submit.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\tune.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\hook.cpp"
|
||||
@@ -43,17 +49,37 @@ set SRC=%SRC% "%ROOT%src\vr\input_labels.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\lang.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\hud_capture.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\perf_cpu.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\physics\body_push.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\physics\body_volumes.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\godmode\god_carry.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\godmode\god_pluck.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\physics\soft_body.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\physics\rear_jiggle.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\physics\rear_shape.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\godmode\board_leash.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\menu_panel.cpp"
|
||||
set SRC=%SRC% "%ROOT%src\vr\finger_input.cpp"
|
||||
|
||||
rem OpenVR headers only. openvr_api.dll is loaded at runtime rather than
|
||||
rem linked, so a missing or broken VR runtime cannot stop the game starting.
|
||||
rem OpenXR (the default backend): headers and the import library of
|
||||
rem openxr_loader.dll (third_party\openxr, built from the Khronos 1.1.63
|
||||
rem source). The DLL is delay-loaded, so without it the game still starts and
|
||||
rem vr_runtime.cpp falls back to OpenVR.
|
||||
set "OPENXR=%ROOT%third_party\openxr\include"
|
||||
rem OpenVR headers (the fallback backend, ovr_runtime.cpp); openvr_api.dll is
|
||||
rem loaded at run time, beside the game.
|
||||
set "OPENVR=%ROOT%third_party\openvr\headers"
|
||||
rem The VR interop header lives in our DXVK fork and pulls in Vulkan.
|
||||
rem Dear ImGui (third_party\imgui): the in-game settings overlay.
|
||||
set "IMGUI=%ROOT%third_party\imgui"
|
||||
for %%F in (imgui imgui_draw imgui_tables imgui_widgets imgui_impl_dx9 imgui_impl_win32) do call set SRC=%%SRC%% "%IMGUI%\%%F.cpp"
|
||||
set "OPENXRLIB=%ROOT%third_party\openxr\lib\win32\openxr_loader.lib"
|
||||
rem The VR interop header lives in our DXVK fork and pulls in Vulkan. The
|
||||
rem headers the fork vendors are used unless a Vulkan SDK is installed.
|
||||
set "DXVKVR=%ROOT%dxvk-trlvr\src\d3d9"
|
||||
set "VKINC="
|
||||
set "VKINC=%ROOT%dxvk-trlvr\include\vulkan\include"
|
||||
for /d %%D in ("C:\VulkanSDK\*") do set "VKINC=%%D\Include"
|
||||
|
||||
set CFLAGS=/nologo /c /EHsc /MT /O2 /W4 /std:c++17 /DWIN32 /DNDEBUG /D_CRT_SECURE_NO_WARNINGS /I"%OPENVR%" /I"%DXVKVR%" /I"%VKINC%" /Fo"%OUT%\\"
|
||||
set LFLAGS=/nologo /DLL /MACHINE:X86 /DEF:"%ROOT%src\proxy\d3d9.def" /OUT:"%OUT%\d3d9.dll"
|
||||
set CFLAGS=/nologo /c /EHsc /MT /O2 /W4 /std:c++17 /DWIN32 /DNDEBUG /D_CRT_SECURE_NO_WARNINGS /DIMGUI_IMPL_WIN32_DISABLE_GAMEPAD /I"%OPENXR%" /I"%OPENVR%" /I"%IMGUI%" /I"%DXVKVR%" /I"%VKINC%" /Fo"%OUT%\\"
|
||||
set LFLAGS=/nologo /DLL /MACHINE:X86 /DEF:"%ROOT%src\proxy\d3d9.def" /OUT:"%OUT%\d3d9.dll" /MAP:"%OUT%\d3d9.map"
|
||||
|
||||
echo Compiling...
|
||||
del /q "%OUT%\*.obj" 2>nul
|
||||
@@ -68,7 +94,7 @@ rem Deliberately NOT linking d3d9.lib: this DLL *is* d3d9.dll, and an import
|
||||
rem library would make it import from itself. Every entry point is resolved
|
||||
rem from the backend with GetProcAddress instead. dxguid.lib supplies the
|
||||
rem interface IIDs only, and pulls in no DLL.
|
||||
link %LFLAGS% "%OUT%\*.obj" dxguid.lib user32.lib kernel32.lib
|
||||
link %LFLAGS% "%OUT%\*.obj" "%OPENXRLIB%" /DELAYLOAD:openxr_loader.dll delayimp.lib dxguid.lib user32.lib kernel32.lib advapi32.lib
|
||||
if errorlevel 1 (
|
||||
echo BUILD FAILED ^(link^)
|
||||
exit /b 1
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
Copyright (c) 2015, Valve Corporation
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -228,6 +228,7 @@ namespace dxvk {
|
||||
friend class D3D9UserDefinedAnnotation;
|
||||
friend class DxvkLegacyD3DDeviceBridge;
|
||||
friend D3D9VkInteropDevice;
|
||||
friend class D3D9VR;
|
||||
|
||||
using CbvIndex = D3D9ShaderResourceMapping::CbvIndex;
|
||||
public:
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace dxvk {
|
||||
pDesc->PhysicalDevice = device->adapter()->handle();
|
||||
pDesc->Instance = device->instance()->handle();
|
||||
pDesc->Queue = device->queues().graphics.queueHandle;
|
||||
pDesc->QueueFamilyIndex = device->queues().graphics.queueIndex;
|
||||
pDesc->QueueFamilyIndex = device->queues().graphics.queueFamily;
|
||||
|
||||
pDesc->Width = desc->Width;
|
||||
pDesc->Height = desc->Height;
|
||||
@@ -177,11 +177,209 @@ namespace dxvk {
|
||||
return D3D_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE GetDeviceDesc(
|
||||
D3D9_VR_DEVICE_DESC* pDesc) {
|
||||
if (unlikely(pDesc == nullptr))
|
||||
return D3DERR_INVALIDCALL;
|
||||
|
||||
const auto& device = m_device->GetDXVKDevice();
|
||||
|
||||
pDesc->Instance = device->instance()->handle();
|
||||
pDesc->PhysicalDevice = device->adapter()->handle();
|
||||
pDesc->Device = device->handle();
|
||||
pDesc->Queue = device->queues().graphics.queueHandle;
|
||||
pDesc->QueueFamilyIndex = device->queues().graphics.queueFamily;
|
||||
pDesc->QueueIndex = device->queues().graphics.queueIndex;
|
||||
return D3D_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CopySurfaceToImage(
|
||||
IDirect3DSurface9* pSurface,
|
||||
const D3D9_VR_EXTERNAL_IMAGE* pDst,
|
||||
INT DstX,
|
||||
INT DstY,
|
||||
BOOL WaitForCopy) {
|
||||
if (unlikely(pSurface == nullptr || pDst == nullptr || !pDst->Image))
|
||||
return D3DERR_INVALIDCALL;
|
||||
|
||||
D3D9DeviceLock lock = m_device->LockDevice();
|
||||
|
||||
auto* tex = static_cast<D3D9Surface*>(pSurface)->GetCommonTexture();
|
||||
if (unlikely(tex == nullptr))
|
||||
return D3DERR_INVALIDCALL;
|
||||
|
||||
const Rc<DxvkImage> srcImage = tex->GetImage();
|
||||
if (unlikely(srcImage == nullptr
|
||||
|| srcImage->info().sampleCount != VK_SAMPLE_COUNT_1_BIT))
|
||||
return D3DERR_INVALIDCALL;
|
||||
|
||||
const VkExtent3D extent = srcImage->mipLevelExtent(0);
|
||||
|
||||
if (unlikely(DstX < 0 || DstY < 0
|
||||
|| uint32_t(DstX) + extent.width > pDst->Width
|
||||
|| uint32_t(DstY) + extent.height > pDst->Height))
|
||||
return D3DERR_INVALIDCALL;
|
||||
|
||||
Rc<DxvkImage> dstImage = WrapExternalImage(*pDst);
|
||||
if (unlikely(dstImage == nullptr))
|
||||
return D3DERR_INVALIDCALL;
|
||||
|
||||
const VkImageSubresourceLayers layers = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
|
||||
const VkFormat srcFormat = srcImage->info().format;
|
||||
|
||||
if (LinearFormat(srcFormat) == LinearFormat(pDst->Format)) {
|
||||
// Same channel layout: the bits go across unchanged, which is what
|
||||
// is wanted even from UNORM to SRGB -- the game's back buffer holds
|
||||
// gamma-encoded colour in a UNORM image.
|
||||
m_device->EmitCs([
|
||||
cDstImage = dstImage,
|
||||
cSrcImage = srcImage,
|
||||
cLayers = layers,
|
||||
cDstOffset = VkOffset3D { DstX, DstY, 0 },
|
||||
cExtent = extent
|
||||
] (DxvkContext* ctx) {
|
||||
ctx->copyImage(
|
||||
cDstImage, cLayers, cDstOffset,
|
||||
cSrcImage, cLayers, VkOffset3D { 0, 0, 0 },
|
||||
cExtent);
|
||||
});
|
||||
} else {
|
||||
// Different channel order (BGRA into RGBA): a blit. Reading the
|
||||
// source through its sRGB view when the destination is sRGB makes
|
||||
// the decode and the encode cancel, so the colours still match.
|
||||
if (!m_blitReported) {
|
||||
m_blitReported = true;
|
||||
Logger::warn(str::format("D3D9VR: source format ", srcFormat,
|
||||
" and swapchain format ", pDst->Format,
|
||||
" differ in layout; blitting instead of copying"));
|
||||
}
|
||||
|
||||
const VkFormat srcSrgb = tex->GetFormatMapping().FormatSrgb;
|
||||
|
||||
DxvkImageViewKey dstViewInfo;
|
||||
dstViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
dstViewInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
dstViewInfo.format = pDst->Format;
|
||||
dstViewInfo.aspects = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
dstViewInfo.mipIndex = 0;
|
||||
dstViewInfo.mipCount = 1;
|
||||
dstViewInfo.layerIndex = 0;
|
||||
dstViewInfo.layerCount = 1;
|
||||
|
||||
DxvkImageViewKey srcViewInfo = dstViewInfo;
|
||||
srcViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
srcViewInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
srcViewInfo.format = (IsSrgbFormat(pDst->Format) && srcSrgb)
|
||||
? srcSrgb : srcFormat;
|
||||
|
||||
std::array<VkOffset3D, 2> dstOffsets = {{
|
||||
{ DstX, DstY, 0 },
|
||||
{ DstX + int32_t(extent.width), DstY + int32_t(extent.height), 1 } }};
|
||||
std::array<VkOffset3D, 2> srcOffsets = {{
|
||||
{ 0, 0, 0 },
|
||||
{ int32_t(extent.width), int32_t(extent.height), 1 } }};
|
||||
|
||||
m_device->EmitCs([
|
||||
cDstView = dstImage->createView(dstViewInfo),
|
||||
cSrcView = srcImage->createView(srcViewInfo),
|
||||
cDstOffsets = dstOffsets,
|
||||
cSrcOffsets = srcOffsets
|
||||
] (DxvkContext* ctx) {
|
||||
ctx->blitImageView(
|
||||
cDstView, cDstOffsets.data(),
|
||||
cSrcView, cSrcOffsets.data(),
|
||||
VK_FILTER_NEAREST);
|
||||
});
|
||||
}
|
||||
|
||||
if (WaitForCopy) {
|
||||
m_device->WaitForResource(*dstImage,
|
||||
DxvkCsThread::SynchronizeAll, 0);
|
||||
}
|
||||
|
||||
return D3D_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE ReleaseExternalImages() {
|
||||
if (m_externalImages.empty())
|
||||
return D3D_OK;
|
||||
|
||||
// Nothing may still be reading or writing them when the runtime
|
||||
// destroys the swapchain behind these handles.
|
||||
D3D9DeviceLock lock = m_device->LockDevice();
|
||||
|
||||
m_device->Flush();
|
||||
m_device->SynchronizeCsThread(DxvkCsThread::SynchronizeAll);
|
||||
m_device->GetDXVKDevice()->waitForIdle();
|
||||
m_externalImages.clear();
|
||||
return D3D_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
static VkFormat LinearFormat(VkFormat Format) {
|
||||
switch (Format) {
|
||||
case VK_FORMAT_B8G8R8A8_SRGB: return VK_FORMAT_B8G8R8A8_UNORM;
|
||||
case VK_FORMAT_R8G8B8A8_SRGB: return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
default: return Format;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsSrgbFormat(VkFormat Format) {
|
||||
return LinearFormat(Format) != Format;
|
||||
}
|
||||
|
||||
// DXVK only copies between images it knows, so each swapchain image is
|
||||
// imported once. shared makes DXVK return it to its default layout at
|
||||
// the end of every submission, which is the layout the runtime expects
|
||||
// it released in.
|
||||
Rc<DxvkImage> WrapExternalImage(const D3D9_VR_EXTERNAL_IMAGE& Image) {
|
||||
for (const auto& entry : m_externalImages) {
|
||||
if (entry.first == Image.Image)
|
||||
return entry.second;
|
||||
}
|
||||
|
||||
DxvkImageCreateInfo info;
|
||||
info.type = VK_IMAGE_TYPE_2D;
|
||||
info.format = Image.Format;
|
||||
info.flags = 0u;
|
||||
info.sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
info.extent = { Image.Width, Image.Height, 1u };
|
||||
info.numLayers = 1u;
|
||||
info.mipLevels = 1u;
|
||||
info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT
|
||||
| VK_IMAGE_USAGE_TRANSFER_SRC_BIT
|
||||
| VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
|
||||
| VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
info.stages = VK_PIPELINE_STAGE_TRANSFER_BIT
|
||||
| VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
|
||||
| VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
||||
info.access = VK_ACCESS_TRANSFER_WRITE_BIT
|
||||
| VK_ACCESS_TRANSFER_READ_BIT
|
||||
| VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT
|
||||
| VK_ACCESS_COLOR_ATTACHMENT_READ_BIT
|
||||
| VK_ACCESS_SHADER_READ_BIT;
|
||||
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
info.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
info.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
info.shared = VK_TRUE;
|
||||
info.debugName = "OpenXR swapchain image";
|
||||
|
||||
Rc<DxvkImage> image = m_device->GetDXVKDevice()->importImage(
|
||||
info, VkImage(Image.Image), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
|
||||
m_externalImages.emplace_back(Image.Image, image);
|
||||
Logger::info(str::format("D3D9VR: imported ", Image.Width, "x",
|
||||
Image.Height, " swapchain image, format ", Image.Format));
|
||||
return image;
|
||||
}
|
||||
|
||||
D3D9DeviceEx* m_device;
|
||||
D3D9DeviceLock m_lock;
|
||||
|
||||
std::vector<std::pair<uint64_t, Rc<DxvkImage>>> m_externalImages;
|
||||
bool m_blitReported = false;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@@ -192,6 +390,8 @@ extern "C" HRESULT __stdcall Direct3DCreateVR9(
|
||||
if (pDevice == nullptr || ppInterface == nullptr)
|
||||
return D3DERR_INVALIDCALL;
|
||||
|
||||
*ppInterface = new dxvk::D3D9VR(pDevice);
|
||||
// DXVK's COM objects start with no references; hand this one out owned,
|
||||
// so the caller's QueryInterface / Release pair cannot destroy it.
|
||||
*ppInterface = dxvk::ref(new dxvk::D3D9VR(pDevice));
|
||||
return D3D_OK;
|
||||
}
|
||||
@@ -12,6 +12,11 @@
|
||||
// vr::VRVulkanTextureData_t, so a filled desc can be memcpy'd or cast straight
|
||||
// into a Submit call.
|
||||
//
|
||||
// The mod now runs on OpenXR, whose swapchain images belong to the runtime:
|
||||
// the session is created from GetDeviceDesc and each frame is copied in with
|
||||
// CopySurfaceToImage. GetVRDesc and TransferSurface are the OpenVR-era
|
||||
// hand-over and are kept for a runtime that takes the image in place.
|
||||
//
|
||||
// The design follows the d3d9_vr interface in the L4D2VR and SiN VR DXVK forks.
|
||||
// This is a fresh implementation against DXVK 3.1, whose device-locking API
|
||||
// differs from the 3.0.2 those were written for.
|
||||
@@ -36,7 +41,26 @@ struct D3D9_TEXTURE_VR_DESC {
|
||||
uint32_t SampleCount;
|
||||
};
|
||||
|
||||
MIDL_INTERFACE("7d4a1e93-2c68-4b51-9a0f-6e83c5d27a14")
|
||||
// What an OpenXR session is created from (XrGraphicsBindingVulkanKHR).
|
||||
struct D3D9_VR_DEVICE_DESC {
|
||||
VkInstance Instance;
|
||||
VkPhysicalDevice PhysicalDevice;
|
||||
VkDevice Device;
|
||||
VkQueue Queue;
|
||||
uint32_t QueueFamilyIndex;
|
||||
uint32_t QueueIndex; // within the family
|
||||
};
|
||||
|
||||
// A VkImage DXVK does not own: an OpenXR swapchain image. The runtime hands
|
||||
// these out in COLOR_ATTACHMENT_OPTIMAL and wants them back that way.
|
||||
struct D3D9_VR_EXTERNAL_IMAGE {
|
||||
uint64_t Image;
|
||||
VkFormat Format;
|
||||
uint32_t Width;
|
||||
uint32_t Height;
|
||||
};
|
||||
|
||||
MIDL_INTERFACE("b1e5c0a4-3f7d-4a92-8c16-2d9e47f0a3b5")
|
||||
IDirect3DVR9 : public IUnknown {
|
||||
// Fills pDesc with the Vulkan handles backing pSurface.
|
||||
virtual HRESULT STDMETHODCALLTYPE GetVRDesc(
|
||||
@@ -58,6 +82,29 @@ IDirect3DVR9 : public IUnknown {
|
||||
virtual HRESULT STDMETHODCALLTYPE UnlockSubmissionQueue() = 0;
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE WaitDeviceIdle() = 0;
|
||||
|
||||
// --- OpenXR ---------------------------------------------------------------
|
||||
// OpenXR swapchain images belong to the runtime, so a frame is copied into
|
||||
// one rather than submitted in place. The copy is recorded through DXVK's
|
||||
// own context, which keeps its layout and lifetime tracking correct and
|
||||
// puts the copy on the queue in order with the frame it reads.
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDeviceDesc(
|
||||
D3D9_VR_DEVICE_DESC* pDesc) = 0;
|
||||
|
||||
// Copies all of pSurface to (DstX, DstY) in the external image. With
|
||||
// WaitForCopy the call returns once the GPU has finished it. Must not be
|
||||
// called with the submission queue locked.
|
||||
virtual HRESULT STDMETHODCALLTYPE CopySurfaceToImage(
|
||||
IDirect3DSurface9* pSurface,
|
||||
const D3D9_VR_EXTERNAL_IMAGE* pDst,
|
||||
INT DstX,
|
||||
INT DstY,
|
||||
BOOL WaitForCopy) = 0;
|
||||
|
||||
// Drops every wrapper made for an external image, after the GPU is done
|
||||
// with them. Call before the runtime destroys the swapchain.
|
||||
virtual HRESULT STDMETHODCALLTYPE ReleaseExternalImages() = 0;
|
||||
};
|
||||
|
||||
// NOTE: the locks above only lock anything if the device was created with
|
||||
@@ -67,9 +114,9 @@ IDirect3DVR9 : public IUnknown {
|
||||
// CreateDevice rather than this fork weakening DXVK's locking.
|
||||
|
||||
#ifdef _MSC_VER
|
||||
struct __declspec(uuid("7d4a1e93-2c68-4b51-9a0f-6e83c5d27a14")) IDirect3DVR9;
|
||||
struct __declspec(uuid("b1e5c0a4-3f7d-4a92-8c16-2d9e47f0a3b5")) IDirect3DVR9;
|
||||
#else
|
||||
__CRT_UUID_DECL(IDirect3DVR9, 0x7d4a1e93, 0x2c68, 0x4b51, 0x9a, 0x0f, 0x6e, 0x83, 0xc5, 0xd2, 0x7a, 0x14);
|
||||
__CRT_UUID_DECL(IDirect3DVR9, 0xb1e5c0a4, 0x3f7d, 0x4a92, 0x8c, 0x16, 0x2d, 0x9e, 0x47, 0xf0, 0xa3, 0xb5);
|
||||
#endif
|
||||
|
||||
// Exported from d3d9.dll. Resolve with GetProcAddress and pass the live device.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include <buildenv.h>
|
||||
|
||||
#include "dxvk_instance.h"
|
||||
#include "dxvk_openvr.h"
|
||||
#include "dxvk_openxr.h"
|
||||
#include "dxvk_platform_exts.h"
|
||||
|
||||
@@ -42,7 +41,6 @@ namespace dxvk {
|
||||
// Initialize extension providers
|
||||
m_extProviders.push_back(&DxvkPlatformExts::s_instance);
|
||||
#ifdef _WIN32
|
||||
m_extProviders.push_back(&VrInstance::s_instance);
|
||||
m_extProviders.push_back(&DxvkXrProvider::s_instance);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
#include "dxvk_instance.h"
|
||||
#include "dxvk_openvr.h"
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic ignored "-Wnon-virtual-dtor"
|
||||
#endif
|
||||
|
||||
#include <openvr/openvr.hpp>
|
||||
|
||||
using VR_InitInternalProc = vr::IVRSystem* (VR_CALLTYPE *)(vr::EVRInitError*, vr::EVRApplicationType);
|
||||
using VR_ShutdownInternalProc = void (VR_CALLTYPE *)();
|
||||
using VR_GetGenericInterfaceProc = void* (VR_CALLTYPE *)(const char*, vr::EVRInitError*);
|
||||
|
||||
namespace dxvk {
|
||||
|
||||
struct VrFunctions {
|
||||
VR_InitInternalProc initInternal = nullptr;
|
||||
VR_ShutdownInternalProc shutdownInternal = nullptr;
|
||||
VR_GetGenericInterfaceProc getGenericInterface = nullptr;
|
||||
};
|
||||
|
||||
VrFunctions g_vrFunctions;
|
||||
VrInstance VrInstance::s_instance;
|
||||
|
||||
VrInstance:: VrInstance() {
|
||||
m_no_vr = env::getEnvVar("DXVK_NO_VR") == "1";
|
||||
}
|
||||
VrInstance::~VrInstance() { }
|
||||
|
||||
|
||||
std::string_view VrInstance::getName() {
|
||||
return "OpenVR";
|
||||
}
|
||||
|
||||
|
||||
DxvkExtensionList VrInstance::getInstanceExtensions() {
|
||||
std::lock_guard<dxvk::mutex> lock(m_mutex);
|
||||
return m_insExtensions;
|
||||
}
|
||||
|
||||
|
||||
DxvkExtensionList VrInstance::getDeviceExtensions(uint32_t adapterId) {
|
||||
std::lock_guard<dxvk::mutex> lock(m_mutex);
|
||||
|
||||
if (adapterId < m_devExtensions.size())
|
||||
return m_devExtensions[adapterId];
|
||||
|
||||
return DxvkExtensionList();
|
||||
}
|
||||
|
||||
|
||||
void VrInstance::initInstanceExtensions() {
|
||||
std::lock_guard<dxvk::mutex> lock(m_mutex);
|
||||
|
||||
if (m_no_vr || m_initializedDevExt)
|
||||
return;
|
||||
|
||||
if (!m_vr_key)
|
||||
{
|
||||
LSTATUS status;
|
||||
|
||||
if ((status = RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\Wine\\VR", 0, KEY_READ, &m_vr_key)))
|
||||
Logger::info(str::format("OpenVR: could not open registry key, status ", status));
|
||||
}
|
||||
|
||||
if (!m_vr_key && !m_compositor)
|
||||
m_compositor = this->getCompositor();
|
||||
|
||||
if (!m_vr_key && !m_compositor)
|
||||
return;
|
||||
|
||||
m_insExtensions = this->queryInstanceExtensions();
|
||||
m_initializedInsExt = true;
|
||||
}
|
||||
|
||||
|
||||
void VrInstance::initDeviceExtensions(const DxvkInstance* instance) {
|
||||
std::lock_guard<dxvk::mutex> lock(m_mutex);
|
||||
|
||||
if (m_no_vr || (!m_vr_key && !m_compositor) || m_initializedDevExt)
|
||||
return;
|
||||
|
||||
for (uint32_t i = 0; instance->enumAdapters(i) != nullptr; i++) {
|
||||
m_devExtensions.push_back(this->queryDeviceExtensions(
|
||||
instance->enumAdapters(i)));
|
||||
}
|
||||
|
||||
m_initializedDevExt = true;
|
||||
this->shutdown();
|
||||
}
|
||||
|
||||
bool VrInstance::waitVrKeyReady() const {
|
||||
DWORD type, value, wait_status, size;
|
||||
LSTATUS status;
|
||||
HANDLE event;
|
||||
|
||||
size = sizeof(value);
|
||||
if ((status = RegQueryValueExA(m_vr_key, "state", nullptr, &type, reinterpret_cast<BYTE*>(&value), &size)))
|
||||
{
|
||||
Logger::err(str::format("OpenVR: could not query value, status ", status));
|
||||
return false;
|
||||
}
|
||||
if (type != REG_DWORD)
|
||||
{
|
||||
Logger::err(str::format("OpenVR: unexpected value type ", type));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value)
|
||||
return value == 1;
|
||||
|
||||
event = CreateEventA(nullptr, FALSE, FALSE, nullptr);
|
||||
while (1)
|
||||
{
|
||||
if (RegNotifyChangeKeyValue(m_vr_key, FALSE, REG_NOTIFY_CHANGE_LAST_SET, event, TRUE))
|
||||
{
|
||||
Logger::err("Error registering registry change notification");
|
||||
goto done;
|
||||
}
|
||||
size = sizeof(value);
|
||||
if ((status = RegQueryValueExA(m_vr_key, "state", nullptr, &type, reinterpret_cast<BYTE*>(&value), &size)))
|
||||
{
|
||||
Logger::err(str::format("OpenVR: could not query value, status ", status));
|
||||
goto done;
|
||||
}
|
||||
if (value)
|
||||
break;
|
||||
while ((wait_status = WaitForSingleObject(event, 1000)) == WAIT_TIMEOUT)
|
||||
Logger::warn("VR state wait timeout (retrying)");
|
||||
|
||||
if (wait_status != WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::err(str::format("Got unexpected wait status ", wait_status));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
CloseHandle(event);
|
||||
return value == 1;
|
||||
}
|
||||
|
||||
DxvkExtensionList VrInstance::queryInstanceExtensions() const {
|
||||
std::vector<char> extensionList;
|
||||
DWORD len;
|
||||
|
||||
if (m_vr_key)
|
||||
{
|
||||
LSTATUS status;
|
||||
DWORD type;
|
||||
|
||||
if (!this->waitVrKeyReady())
|
||||
return DxvkExtensionList();
|
||||
|
||||
len = 0;
|
||||
if ((status = RegQueryValueExA(m_vr_key, "openvr_vulkan_instance_extensions", nullptr, &type, nullptr, &len)))
|
||||
{
|
||||
Logger::err(str::format("OpenVR: could not query value, status ", status));
|
||||
return DxvkExtensionList();
|
||||
}
|
||||
extensionList.resize(len);
|
||||
if ((status = RegQueryValueExA(m_vr_key, "openvr_vulkan_instance_extensions", nullptr, &type, reinterpret_cast<BYTE*>(extensionList.data()), &len)))
|
||||
{
|
||||
Logger::err(str::format("OpenVR: could not query value, status ", status));
|
||||
return DxvkExtensionList();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
len = m_compositor->GetVulkanInstanceExtensionsRequired(nullptr, 0);
|
||||
extensionList.resize(len);
|
||||
len = m_compositor->GetVulkanInstanceExtensionsRequired(extensionList.data(), len);
|
||||
}
|
||||
return parseExtensionList(std::string(extensionList.data(), len));
|
||||
}
|
||||
|
||||
|
||||
DxvkExtensionList VrInstance::queryDeviceExtensions(Rc<DxvkAdapter> adapter) const {
|
||||
std::vector<char> extensionList;
|
||||
DWORD len;
|
||||
|
||||
if (m_vr_key) {
|
||||
LSTATUS status;
|
||||
char name[256];
|
||||
DWORD type;
|
||||
|
||||
if (!this->waitVrKeyReady())
|
||||
return DxvkExtensionList();
|
||||
|
||||
auto adapterInfo = adapter->info();
|
||||
sprintf(name, "PCIID:%04x:%04x", adapterInfo.vendorId, adapterInfo.deviceId);
|
||||
|
||||
len = 0;
|
||||
|
||||
if ((status = RegQueryValueExA(m_vr_key, name, nullptr, &type, nullptr, &len))) {
|
||||
Logger::err(str::format("OpenVR: could not query value, status ", status));
|
||||
return DxvkExtensionList();
|
||||
}
|
||||
|
||||
extensionList.resize(len);
|
||||
|
||||
if ((status = RegQueryValueExA(m_vr_key, name, nullptr, &type, reinterpret_cast<BYTE*>(extensionList.data()), &len))) {
|
||||
Logger::err(str::format("OpenVR: could not query value, status ", status));
|
||||
return DxvkExtensionList();
|
||||
}
|
||||
} else {
|
||||
len = m_compositor->GetVulkanDeviceExtensionsRequired(adapter->handle(), nullptr, 0);
|
||||
extensionList.resize(len);
|
||||
len = m_compositor->GetVulkanDeviceExtensionsRequired(adapter->handle(), extensionList.data(), len);
|
||||
}
|
||||
|
||||
return parseExtensionList(std::string(extensionList.data(), len));
|
||||
}
|
||||
|
||||
|
||||
DxvkExtensionList VrInstance::parseExtensionList(const std::string& str) const {
|
||||
DxvkExtensionList result;
|
||||
|
||||
std::stringstream strstream(str);
|
||||
std::string section;
|
||||
|
||||
while (std::getline(strstream, section, ' '))
|
||||
result.push_back(vk::makeExtension(section.c_str()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
vr::IVRCompositor* VrInstance::getCompositor() {
|
||||
// Skip OpenVR initialization if requested
|
||||
|
||||
// Locate the OpenVR DLL if loaded by the process. Some
|
||||
// applications may not have OpenVR loaded at the time
|
||||
// they create the DXGI instance, so we try our own DLL.
|
||||
m_ovrApi = this->loadLibrary();
|
||||
|
||||
if (!m_ovrApi) {
|
||||
Logger::info("OpenVR: Failed to locate module");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Load method used to retrieve the IVRCompositor interface
|
||||
g_vrFunctions.initInternal = reinterpret_cast<VR_InitInternalProc> (this->getSym("VR_InitInternal"));
|
||||
g_vrFunctions.shutdownInternal = reinterpret_cast<VR_ShutdownInternalProc> (this->getSym("VR_ShutdownInternal"));
|
||||
g_vrFunctions.getGenericInterface = reinterpret_cast<VR_GetGenericInterfaceProc>(this->getSym("VR_GetGenericInterface"));
|
||||
|
||||
if (!g_vrFunctions.getGenericInterface) {
|
||||
Logger::warn("OpenVR: VR_GetGenericInterface not found");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Retrieve the compositor interface
|
||||
vr::EVRInitError error = vr::VRInitError_None;
|
||||
|
||||
vr::IVRCompositor* compositor = reinterpret_cast<vr::IVRCompositor*>(
|
||||
g_vrFunctions.getGenericInterface(vr::IVRCompositor_Version, &error));
|
||||
|
||||
if (error != vr::VRInitError_None || !compositor) {
|
||||
if (!g_vrFunctions.initInternal
|
||||
|| !g_vrFunctions.shutdownInternal) {
|
||||
Logger::warn("OpenVR: VR_InitInternal or VR_ShutdownInternal not found");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// If the app has not initialized OpenVR yet, we need
|
||||
// to do it now in order to grab a compositor instance
|
||||
g_vrFunctions.initInternal(&error, vr::VRApplication_Background);
|
||||
m_initializedOpenVr = error == vr::VRInitError_None;
|
||||
|
||||
if (error != vr::VRInitError_None) {
|
||||
Logger::warn("OpenVR: Failed to initialize OpenVR");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
compositor = reinterpret_cast<vr::IVRCompositor*>(
|
||||
g_vrFunctions.getGenericInterface(vr::IVRCompositor_Version, &error));
|
||||
|
||||
if (error != vr::VRInitError_None || !compositor) {
|
||||
Logger::warn("OpenVR: Failed to query compositor interface");
|
||||
this->shutdown();
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Logger::info("OpenVR: Compositor interface found");
|
||||
return compositor;
|
||||
}
|
||||
|
||||
|
||||
void VrInstance::shutdown() {
|
||||
if (m_vr_key)
|
||||
{
|
||||
RegCloseKey(m_vr_key);
|
||||
m_vr_key = nullptr;
|
||||
}
|
||||
|
||||
if (m_initializedOpenVr)
|
||||
g_vrFunctions.shutdownInternal();
|
||||
|
||||
if (m_loadedOvrApi)
|
||||
this->freeLibrary();
|
||||
|
||||
m_initializedOpenVr = false;
|
||||
m_loadedOvrApi = false;
|
||||
}
|
||||
|
||||
|
||||
HMODULE VrInstance::loadLibrary() {
|
||||
HMODULE handle;
|
||||
|
||||
// Use openvr_api.dll only if already loaded in the process (and reference it which GetModuleHandleEx does without
|
||||
// GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT flag).
|
||||
if (!::GetModuleHandleEx(0, "openvr_api.dll", &handle))
|
||||
handle = ::LoadLibrary("openvr_api_dxvk.dll");
|
||||
|
||||
m_loadedOvrApi = handle != nullptr;
|
||||
return handle;
|
||||
}
|
||||
|
||||
|
||||
void VrInstance::freeLibrary() {
|
||||
::FreeLibrary(m_ovrApi);
|
||||
}
|
||||
|
||||
|
||||
void* VrInstance::getSym(const char* sym) {
|
||||
return reinterpret_cast<void*>(
|
||||
::GetProcAddress(m_ovrApi, sym));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "dxvk_extension_provider.h"
|
||||
|
||||
namespace vr {
|
||||
class IVRCompositor;
|
||||
class IVRSystem;
|
||||
}
|
||||
|
||||
namespace dxvk {
|
||||
|
||||
class DxvkInstance;
|
||||
|
||||
/**
|
||||
* \brief OpenVR instance
|
||||
*
|
||||
* Loads Initializes OpenVR to provide
|
||||
* access to Vulkan extension queries.
|
||||
*/
|
||||
class VrInstance : public DxvkExtensionProvider {
|
||||
|
||||
public:
|
||||
|
||||
VrInstance();
|
||||
~VrInstance();
|
||||
|
||||
std::string_view getName();
|
||||
|
||||
DxvkExtensionList getInstanceExtensions();
|
||||
|
||||
DxvkExtensionList getDeviceExtensions(
|
||||
uint32_t adapterId);
|
||||
|
||||
void initInstanceExtensions();
|
||||
|
||||
void initDeviceExtensions(
|
||||
const DxvkInstance* instance);
|
||||
|
||||
static VrInstance s_instance;
|
||||
|
||||
private:
|
||||
|
||||
dxvk::mutex m_mutex;
|
||||
HKEY m_vr_key = nullptr;
|
||||
vr::IVRCompositor* m_compositor = nullptr;
|
||||
HMODULE m_ovrApi = nullptr;
|
||||
|
||||
bool m_no_vr;
|
||||
bool m_loadedOvrApi = false;
|
||||
bool m_initializedOpenVr = false;
|
||||
bool m_initializedInsExt = false;
|
||||
bool m_initializedDevExt = false;
|
||||
|
||||
DxvkExtensionList m_insExtensions;
|
||||
std::vector<DxvkExtensionList> m_devExtensions;
|
||||
|
||||
DxvkExtensionList queryInstanceExtensions() const;
|
||||
|
||||
DxvkExtensionList queryDeviceExtensions(
|
||||
Rc<DxvkAdapter> adapter) const;
|
||||
|
||||
DxvkExtensionList parseExtensionList(
|
||||
const std::string& str) const;
|
||||
|
||||
vr::IVRCompositor* getCompositor();
|
||||
|
||||
void shutdown();
|
||||
|
||||
HMODULE loadLibrary();
|
||||
|
||||
void freeLibrary();
|
||||
|
||||
void* getSym(const char* sym);
|
||||
|
||||
bool waitVrKeyReady() const;
|
||||
};
|
||||
|
||||
extern VrInstance g_vrInstance;
|
||||
|
||||
}
|
||||
@@ -43,6 +43,23 @@ namespace dxvk {
|
||||
void DxvkXrProvider::initInstanceExtensions() {
|
||||
std::lock_guard<dxvk::mutex> lock(m_mutex);
|
||||
|
||||
// TRL VR: the proxy owns the OpenXR instance and has already asked the
|
||||
// runtime which Vulkan extensions it needs (XR_KHR_vulkan_enable). It
|
||||
// leaves both lists in the process environment before this DLL creates
|
||||
// its instance, so nothing here has to load OpenXR at all.
|
||||
std::string envInstance = env::getEnvVar("TRLVR_XR_VK_INSTANCE_EXTENSIONS");
|
||||
std::string envDevice = env::getEnvVar("TRLVR_XR_VK_DEVICE_EXTENSIONS");
|
||||
|
||||
if (!envInstance.empty() || !envDevice.empty()) {
|
||||
m_insExtensions = this->parseExtensionList(envInstance);
|
||||
m_devExtensions = this->parseExtensionList(envDevice);
|
||||
m_initializedInsExt = true;
|
||||
m_initializedDevExt = true;
|
||||
Logger::info(str::format("OpenXR: instance extensions from the VR proxy: ", envInstance));
|
||||
Logger::info(str::format("OpenXR: device extensions from the VR proxy: ", envDevice));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_wineOxr)
|
||||
m_wineOxr = this->loadLibrary();
|
||||
|
||||
@@ -130,13 +147,15 @@ namespace dxvk {
|
||||
std::stringstream strstream(str);
|
||||
std::string section;
|
||||
|
||||
while (std::getline(strstream, section, ' '))
|
||||
result.push_back(vk::makeExtension(section.c_str()));
|
||||
|
||||
while (std::getline(strstream, section, ' ')) {
|
||||
if (!section.empty())
|
||||
result.push_back(vk::makeExtension(section.c_str()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void DxvkXrProvider::shutdown() {
|
||||
if (m_loadedOxrApi)
|
||||
this->freeLibrary();
|
||||
|
||||
@@ -92,7 +92,6 @@ dxvk_src = [
|
||||
|
||||
if platform == 'windows'
|
||||
dxvk_src += [
|
||||
'dxvk_openvr.cpp',
|
||||
'dxvk_openxr.cpp',
|
||||
]
|
||||
endif
|
||||
|
||||
+167
-63
@@ -1,76 +1,180 @@
|
||||
{
|
||||
"default_bindings": [
|
||||
{ "controller_type": "hpmotioncontroller", "binding_url": "bindings_hpmotioncontroller.json" },
|
||||
{ "controller_type": "oculus_touch", "binding_url": "bindings_oculus_touch.json" },
|
||||
{ "controller_type": "holographic_controller", "binding_url": "bindings_holographic_controller.json" },
|
||||
{ "controller_type": "knuckles", "binding_url": "bindings_knuckles.json" }
|
||||
{
|
||||
"controller_type": "hpmotioncontroller",
|
||||
"binding_url": "bindings_hpmotioncontroller.json"
|
||||
},
|
||||
{
|
||||
"controller_type": "oculus_touch",
|
||||
"binding_url": "bindings_oculus_touch.json"
|
||||
},
|
||||
{
|
||||
"controller_type": "knuckles",
|
||||
"binding_url": "bindings_knuckles.json"
|
||||
},
|
||||
{
|
||||
"controller_type": "holographic_controller",
|
||||
"binding_url": "bindings_holographic_controller.json"
|
||||
}
|
||||
],
|
||||
|
||||
"actions": [
|
||||
{ "name": "/actions/gameplay/in/move", "type": "vector2" },
|
||||
{ "name": "/actions/gameplay/in/look", "type": "vector2" },
|
||||
|
||||
{ "name": "/actions/gameplay/in/jump", "type": "boolean" },
|
||||
{ "name": "/actions/gameplay/in/interact", "type": "boolean" },
|
||||
{ "name": "/actions/gameplay/in/dive", "type": "boolean" },
|
||||
{ "name": "/actions/gameplay/in/fire", "type": "boolean" },
|
||||
{ "name": "/actions/gameplay/in/grapple", "type": "boolean" },
|
||||
{ "name": "/actions/gameplay/in/grenade", "type": "boolean" },
|
||||
{ "name": "/actions/gameplay/in/aim", "type": "boolean" },
|
||||
{ "name": "/actions/gameplay/in/lockon", "type": "boolean" },
|
||||
{ "name": "/actions/gameplay/in/weapon", "type": "boolean" },
|
||||
{ "name": "/actions/gameplay/in/pause", "type": "boolean" },
|
||||
|
||||
{ "name": "/actions/gameplay/in/medkit", "type": "boolean", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/in/light", "type": "boolean", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/in/flick", "type": "boolean", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/in/binoculars", "type": "boolean", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/in/recentre", "type": "boolean", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/in/gear_tuning", "type": "boolean", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/in/pda", "type": "boolean", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/in/hand_left", "type": "pose", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/in/hand_right", "type": "pose", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/in/skeleton_left", "type": "skeleton", "skeleton": "/skeleton/hand/left", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/in/skeleton_right", "type": "skeleton", "skeleton": "/skeleton/hand/right", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/out/haptic_left", "type": "vibration", "requirement": "optional" },
|
||||
{ "name": "/actions/gameplay/out/haptic_right", "type": "vibration", "requirement": "optional" }
|
||||
{
|
||||
"name": "/actions/trlvr/in/stick_left",
|
||||
"type": "vector2"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/trigger_left",
|
||||
"type": "vector1"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/squeeze_left",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/stick_click_left",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/primary_left",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/secondary_left",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/trackpad_left",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/menu_left",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/trigger_touch_left",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/thumb_touch_left",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/hand_left",
|
||||
"type": "pose"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/hand_xr_left",
|
||||
"type": "pose",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/out/haptic_left",
|
||||
"type": "vibration",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/stick_right",
|
||||
"type": "vector2"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/trigger_right",
|
||||
"type": "vector1"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/squeeze_right",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/stick_click_right",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/primary_right",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/secondary_right",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/trackpad_right",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/menu_right",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/trigger_touch_right",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/thumb_touch_right",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/hand_right",
|
||||
"type": "pose"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/in/hand_xr_right",
|
||||
"type": "pose",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/trlvr/out/haptic_right",
|
||||
"type": "vibration",
|
||||
"requirement": "optional"
|
||||
}
|
||||
],
|
||||
|
||||
"action_sets": [
|
||||
{ "name": "/actions/gameplay", "usage": "single" }
|
||||
{
|
||||
"name": "/actions/trlvr",
|
||||
"usage": "single"
|
||||
}
|
||||
],
|
||||
|
||||
"localization": [
|
||||
{
|
||||
"language_tag": "en_US",
|
||||
|
||||
"/actions/gameplay": "Tomb Raider: Legend",
|
||||
|
||||
"/actions/gameplay/in/move": "Move (W A S D)",
|
||||
"/actions/gameplay/in/look": "Turn the camera (mouse)",
|
||||
"/actions/gameplay/in/jump": "Jump / surface (Space)",
|
||||
"/actions/gameplay/in/interact": "Action / interact / pull grapple (E)",
|
||||
"/actions/gameplay/in/dive": "Dive (F)",
|
||||
"/actions/gameplay/in/fire": "Fire (left mouse)",
|
||||
"/actions/gameplay/in/grapple": "Throw grapple (Q)",
|
||||
"/actions/gameplay/in/grenade": "Grenade / flare (K)",
|
||||
"/actions/gameplay/in/aim": "Accurate aim (Z)",
|
||||
"/actions/gameplay/in/lockon": "Combat lock-on (G)",
|
||||
"/actions/gameplay/in/weapon": "Switch weapons (End)",
|
||||
"/actions/gameplay/in/pause": "Pause (Esc)",
|
||||
"/actions/gameplay/in/medkit": "Health pack (Home)",
|
||||
"/actions/gameplay/in/light": "Personal light (Delete)",
|
||||
"/actions/gameplay/in/flick": "Board finger flick (Y held)",
|
||||
"/actions/gameplay/in/binoculars": "Binoculars (Page Down)",
|
||||
"/actions/gameplay/in/recentre": "Recentre the view",
|
||||
"/actions/gameplay/in/gear_tuning": "Gear placement mode (developer)",
|
||||
"/actions/gameplay/in/pda": "PDA (Tab)",
|
||||
"/actions/gameplay/in/hand_left": "Left hand grip pose",
|
||||
"/actions/gameplay/in/hand_right": "Right hand grip pose",
|
||||
"/actions/gameplay/in/skeleton_left": "Left hand fingers (skeleton)",
|
||||
"/actions/gameplay/in/skeleton_right": "Right hand fingers (skeleton)",
|
||||
"/actions/gameplay/out/haptic_left": "Left handhold cue",
|
||||
"/actions/gameplay/out/haptic_right": "Right handhold cue"
|
||||
"/actions/trlvr": "Tomb Raider: Legend VR",
|
||||
"/actions/trlvr/in/stick_left": "stick left",
|
||||
"/actions/trlvr/in/trigger_left": "trigger left",
|
||||
"/actions/trlvr/in/squeeze_left": "squeeze left",
|
||||
"/actions/trlvr/in/stick_click_left": "stick click left",
|
||||
"/actions/trlvr/in/primary_left": "primary left",
|
||||
"/actions/trlvr/in/secondary_left": "secondary left",
|
||||
"/actions/trlvr/in/trackpad_left": "trackpad left",
|
||||
"/actions/trlvr/in/menu_left": "menu left",
|
||||
"/actions/trlvr/in/trigger_touch_left": "trigger touch left",
|
||||
"/actions/trlvr/in/thumb_touch_left": "thumb touch left",
|
||||
"/actions/trlvr/in/hand_left": "hand left",
|
||||
"/actions/trlvr/in/hand_xr_left": "hand xr left",
|
||||
"/actions/trlvr/out/haptic_left": "haptic left",
|
||||
"/actions/trlvr/in/stick_right": "stick right",
|
||||
"/actions/trlvr/in/trigger_right": "trigger right",
|
||||
"/actions/trlvr/in/squeeze_right": "squeeze right",
|
||||
"/actions/trlvr/in/stick_click_right": "stick click right",
|
||||
"/actions/trlvr/in/primary_right": "primary right",
|
||||
"/actions/trlvr/in/secondary_right": "secondary right",
|
||||
"/actions/trlvr/in/trackpad_right": "trackpad right",
|
||||
"/actions/trlvr/in/menu_right": "menu right",
|
||||
"/actions/trlvr/in/trigger_touch_right": "trigger touch right",
|
||||
"/actions/trlvr/in/thumb_touch_right": "thumb touch right",
|
||||
"/actions/trlvr/in/hand_right": "hand right",
|
||||
"/actions/trlvr/in/hand_xr_right": "hand xr right",
|
||||
"/actions/trlvr/out/haptic_right": "haptic right"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,104 +1,133 @@
|
||||
{
|
||||
"controller_type": "holographic_controller",
|
||||
"description": "Tomb Raider: Legend VR -- original Windows Mixed Reality controllers (trackpads)",
|
||||
"description": "Tomb Raider: Legend VR -- Windows Mixed Reality controllers",
|
||||
"name": "Tomb Raider: Legend VR",
|
||||
"bindings": {
|
||||
"/actions/gameplay": {
|
||||
"skeleton": [
|
||||
{ "output": "/actions/gameplay/in/skeleton_left", "path": "/user/hand/left/input/skeleton/left" },
|
||||
{ "output": "/actions/gameplay/in/skeleton_right", "path": "/user/hand/right/input/skeleton/right" }
|
||||
],
|
||||
"poses": [
|
||||
{ "output": "/actions/gameplay/in/hand_left", "path": "/user/hand/left/pose/grip" },
|
||||
{ "output": "/actions/gameplay/in/hand_right", "path": "/user/hand/right/pose/grip" }
|
||||
],
|
||||
"haptics": [
|
||||
{ "output": "/actions/gameplay/out/haptic_left", "path": "/user/hand/left/output/haptic" },
|
||||
{ "output": "/actions/gameplay/out/haptic_right", "path": "/user/hand/right/output/haptic" }
|
||||
],
|
||||
"/actions/trlvr": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": { "output": "/actions/gameplay/in/move" }
|
||||
"position": {
|
||||
"output": "/actions/trlvr/in/stick_left"
|
||||
},
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/stick_click_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/pause" },
|
||||
"double": { "output": "/actions/gameplay/in/recentre" },
|
||||
"long": { "output": "/actions/gameplay/in/medkit" }
|
||||
"pull": {
|
||||
"output": "/actions/trlvr/in/trigger_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/squeeze_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/trackpad",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/trackpad_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/application_menu",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/menu_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": { "output": "/actions/gameplay/in/look" }
|
||||
"position": {
|
||||
"output": "/actions/trlvr/in/stick_right"
|
||||
},
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/stick_click_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/weapon" },
|
||||
"double": { "output": "/actions/gameplay/in/pda" },
|
||||
"long": { "output": "/actions/gameplay/in/binoculars" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/interact" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/fire" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/grapple" } }
|
||||
"inputs": {
|
||||
"pull": {
|
||||
"output": "/actions/trlvr/in/trigger_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/lockon" } }
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/squeeze_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trackpad",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/jump" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/trackpad",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/dive" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/application_menu",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/aim" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/application_menu",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/flick" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/application_menu",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/grenade" },
|
||||
"long": { "output": "/actions/gameplay/in/light" }
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/trackpad_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/application_menu",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/menu_right"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"poses": [
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_left",
|
||||
"path": "/user/hand/left/pose/grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_xr_left",
|
||||
"path": "/user/hand/left/pose/openxr_grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_right",
|
||||
"path": "/user/hand/right/pose/grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_xr_right",
|
||||
"path": "/user/hand/right/pose/openxr_grip"
|
||||
}
|
||||
],
|
||||
"haptics": [
|
||||
{
|
||||
"output": "/actions/trlvr/out/haptic_left",
|
||||
"path": "/user/hand/left/output/haptic"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/out/haptic_right",
|
||||
"path": "/user/hand/right/output/haptic"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,103 +3,132 @@
|
||||
"description": "Tomb Raider: Legend VR -- HP Reverb G2 controllers",
|
||||
"name": "Tomb Raider: Legend VR",
|
||||
"bindings": {
|
||||
"/actions/gameplay": {
|
||||
"skeleton": [
|
||||
{ "output": "/actions/gameplay/in/skeleton_left", "path": "/user/hand/left/input/skeleton/left" },
|
||||
{ "output": "/actions/gameplay/in/skeleton_right", "path": "/user/hand/right/input/skeleton/right" }
|
||||
],
|
||||
"poses": [
|
||||
{ "output": "/actions/gameplay/in/hand_left", "path": "/user/hand/left/pose/grip" },
|
||||
{ "output": "/actions/gameplay/in/hand_right", "path": "/user/hand/right/pose/grip" }
|
||||
],
|
||||
"haptics": [
|
||||
{ "output": "/actions/gameplay/out/haptic_left", "path": "/user/hand/left/output/haptic" },
|
||||
{ "output": "/actions/gameplay/out/haptic_right", "path": "/user/hand/right/output/haptic" }
|
||||
],
|
||||
"/actions/trlvr": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": { "output": "/actions/gameplay/in/move" }
|
||||
"position": {
|
||||
"output": "/actions/trlvr/in/stick_left"
|
||||
},
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/stick_click_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/pause" },
|
||||
"double": { "output": "/actions/gameplay/in/recentre" },
|
||||
"long": { "output": "/actions/gameplay/in/medkit" }
|
||||
"pull": {
|
||||
"output": "/actions/trlvr/in/trigger_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/squeeze_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/x",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/primary_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/y",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/secondary_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": { "output": "/actions/gameplay/in/look" }
|
||||
"position": {
|
||||
"output": "/actions/trlvr/in/stick_right"
|
||||
},
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/stick_click_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/weapon" },
|
||||
"double": { "output": "/actions/gameplay/in/pda" },
|
||||
"long": { "output": "/actions/gameplay/in/binoculars" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/interact" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/fire" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/grapple" } }
|
||||
"inputs": {
|
||||
"pull": {
|
||||
"output": "/actions/trlvr/in/trigger_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/lockon" } }
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/squeeze_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/a",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/jump" } }
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/primary_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/b",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/dive" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/x",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/aim" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/y",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/flick" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/y",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/grenade" },
|
||||
"long": { "output": "/actions/gameplay/in/light" }
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/secondary_right"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"poses": [
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_left",
|
||||
"path": "/user/hand/left/pose/grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_xr_left",
|
||||
"path": "/user/hand/left/pose/openxr_grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_right",
|
||||
"path": "/user/hand/right/pose/grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_xr_right",
|
||||
"path": "/user/hand/right/pose/openxr_grip"
|
||||
}
|
||||
],
|
||||
"haptics": [
|
||||
{
|
||||
"output": "/actions/trlvr/out/haptic_left",
|
||||
"path": "/user/hand/left/output/haptic"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/out/haptic_right",
|
||||
"path": "/user/hand/right/output/haptic"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+135
-66
@@ -3,110 +3,179 @@
|
||||
"description": "Tomb Raider: Legend VR -- Valve Index controllers",
|
||||
"name": "Tomb Raider: Legend VR",
|
||||
"bindings": {
|
||||
"/actions/gameplay": {
|
||||
"skeleton": [
|
||||
{ "output": "/actions/gameplay/in/skeleton_left", "path": "/user/hand/left/input/skeleton/left" },
|
||||
{ "output": "/actions/gameplay/in/skeleton_right", "path": "/user/hand/right/input/skeleton/right" }
|
||||
],
|
||||
"poses": [
|
||||
{ "output": "/actions/gameplay/in/hand_left", "path": "/user/hand/left/pose/grip" },
|
||||
{ "output": "/actions/gameplay/in/hand_right", "path": "/user/hand/right/pose/grip" }
|
||||
],
|
||||
"haptics": [
|
||||
{ "output": "/actions/gameplay/out/haptic_left", "path": "/user/hand/left/output/haptic" },
|
||||
{ "output": "/actions/gameplay/out/haptic_right", "path": "/user/hand/right/output/haptic" }
|
||||
],
|
||||
"/actions/trlvr": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": { "output": "/actions/gameplay/in/move" }
|
||||
"position": {
|
||||
"output": "/actions/trlvr/in/stick_left"
|
||||
},
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/stick_click_left"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/pause" },
|
||||
"double": { "output": "/actions/gameplay/in/recentre" }
|
||||
"pull": {
|
||||
"output": "/actions/trlvr/in/trigger_left"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/trigger_touch_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/squeeze_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/a",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/primary_left"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/b",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/secondary_left"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/trackpad",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/trackpad_left"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": { "output": "/actions/gameplay/in/look" }
|
||||
"position": {
|
||||
"output": "/actions/trlvr/in/stick_right"
|
||||
},
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/stick_click_right"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/weapon" },
|
||||
"double": { "output": "/actions/gameplay/in/pda" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/interact" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/fire" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/grapple" } }
|
||||
"inputs": {
|
||||
"pull": {
|
||||
"output": "/actions/trlvr/in/trigger_right"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/trigger_touch_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/lockon" } }
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/squeeze_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/a",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/jump" } }
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/primary_right"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/b",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/dive" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/a",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/aim" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/b",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/flick" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/b",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/grenade" },
|
||||
"long": { "output": "/actions/gameplay/in/light" }
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/secondary_right"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/trackpad",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/medkit" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trackpad",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/binoculars" } }
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/trackpad_right"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_right"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"poses": [
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_left",
|
||||
"path": "/user/hand/left/pose/grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_xr_left",
|
||||
"path": "/user/hand/left/pose/openxr_grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_right",
|
||||
"path": "/user/hand/right/pose/grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_xr_right",
|
||||
"path": "/user/hand/right/pose/openxr_grip"
|
||||
}
|
||||
],
|
||||
"haptics": [
|
||||
{
|
||||
"output": "/actions/trlvr/out/haptic_left",
|
||||
"path": "/user/hand/left/output/haptic"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/out/haptic_right",
|
||||
"path": "/user/hand/right/output/haptic"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,105 +1,167 @@
|
||||
{
|
||||
"controller_type": "oculus_touch",
|
||||
"description": "Tomb Raider: Legend VR -- Oculus Touch",
|
||||
"description": "Tomb Raider: Legend VR -- Oculus / Meta Touch controllers",
|
||||
"name": "Tomb Raider: Legend VR",
|
||||
"bindings": {
|
||||
"/actions/gameplay": {
|
||||
"skeleton": [
|
||||
{ "output": "/actions/gameplay/in/skeleton_left", "path": "/user/hand/left/input/skeleton/left" },
|
||||
{ "output": "/actions/gameplay/in/skeleton_right", "path": "/user/hand/right/input/skeleton/right" }
|
||||
],
|
||||
"poses": [
|
||||
{ "output": "/actions/gameplay/in/hand_left", "path": "/user/hand/left/pose/grip" },
|
||||
{ "output": "/actions/gameplay/in/hand_right", "path": "/user/hand/right/pose/grip" }
|
||||
],
|
||||
"haptics": [
|
||||
{ "output": "/actions/gameplay/out/haptic_left", "path": "/user/hand/left/output/haptic" },
|
||||
{ "output": "/actions/gameplay/out/haptic_right", "path": "/user/hand/right/output/haptic" }
|
||||
],
|
||||
"/actions/trlvr": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": { "output": "/actions/gameplay/in/move" }
|
||||
"position": {
|
||||
"output": "/actions/trlvr/in/stick_left"
|
||||
},
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/stick_click_left"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/pause" },
|
||||
"double": { "output": "/actions/gameplay/in/recentre" },
|
||||
"long": { "output": "/actions/gameplay/in/medkit" }
|
||||
"pull": {
|
||||
"output": "/actions/trlvr/in/trigger_left"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/trigger_touch_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/squeeze_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/x",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/primary_left"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/y",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/secondary_left"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/application_menu",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/menu_left"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": { "output": "/actions/gameplay/in/look" }
|
||||
"position": {
|
||||
"output": "/actions/trlvr/in/stick_right"
|
||||
},
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/stick_click_right"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/weapon" },
|
||||
"double": { "output": "/actions/gameplay/in/pda" },
|
||||
"long": { "output": "/actions/gameplay/in/binoculars" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/interact" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trigger",
|
||||
"mode": "trigger",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/fire" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/grapple" } }
|
||||
"inputs": {
|
||||
"pull": {
|
||||
"output": "/actions/trlvr/in/trigger_right"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/trigger_touch_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/grip",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/lockon" } }
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/squeeze_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/a",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/jump" } }
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/primary_right"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_right"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/b",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/dive" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/x",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/aim" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/y",
|
||||
"mode": "button",
|
||||
"inputs": { "click": { "output": "/actions/gameplay/in/flick" } }
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/y",
|
||||
"mode": "complex_button",
|
||||
"parameters": { "long_press_delay": "0.65", "long_press_expiry": "3.0" },
|
||||
"inputs": {
|
||||
"single": { "output": "/actions/gameplay/in/grenade" },
|
||||
"long": { "output": "/actions/gameplay/in/light" }
|
||||
"click": {
|
||||
"output": "/actions/trlvr/in/secondary_right"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/trlvr/in/thumb_touch_right"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"poses": [
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_left",
|
||||
"path": "/user/hand/left/pose/grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_xr_left",
|
||||
"path": "/user/hand/left/pose/openxr_grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_right",
|
||||
"path": "/user/hand/right/pose/grip"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/in/hand_xr_right",
|
||||
"path": "/user/hand/right/pose/openxr_grip"
|
||||
}
|
||||
],
|
||||
"haptics": [
|
||||
{
|
||||
"output": "/actions/trlvr/out/haptic_left",
|
||||
"path": "/user/hand/left/output/haptic"
|
||||
},
|
||||
{
|
||||
"output": "/actions/trlvr/out/haptic_right",
|
||||
"path": "/user/hand/right/output/haptic"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+12
-16
@@ -52,14 +52,11 @@ rem original is never touched, so Steam's integrity check has nothing to undo.
|
||||
if exist "!ROOT!dist\trlvr_launcher.exe" (
|
||||
copy /y "!ROOT!dist\trlvr_launcher.exe" "!GAME!\trlvr_launcher.exe" >nul
|
||||
|
||||
rem The controller mapping. Named trlvr_actions.json beside the game so the
|
||||
rem bindings files it names sit next to it -- SteamVR resolves those relative
|
||||
rem to the manifest. Anyone can rebind these in SteamVR s own bindings UI
|
||||
rem without touching the mod.
|
||||
for %%F in (actions.json bindings_hpmotioncontroller.json bindings_holographic_controller.json bindings_oculus_touch.json bindings_knuckles.json) do (
|
||||
if exist "!ROOT!input\%%F" copy /y "!ROOT!input\%%F" "!GAME!\%%F" >nul
|
||||
)
|
||||
if exist "!GAME!\actions.json" move /y "!GAME!\actions.json" "!GAME!\trlvr_actions.json" >nul
|
||||
rem OpenVR (the fallback backend, 1.1.0): the SteamVR action manifest and
|
||||
rem its bindings sit beside the game -- SteamVR resolves the bindings files
|
||||
rem relative to the manifest. OpenXR's bindings are built into d3d9.dll.
|
||||
if exist "!ROOT!input\actions.json" copy /y "!ROOT!input\actions.json" "!GAME!\trlvr_actions.json" >nul
|
||||
for %%F in ("!ROOT!input\bindings_*.json") do copy /y "%%~F" "!GAME!\%%~nxF" >nul
|
||||
|
||||
rem The XInput proxy stopped shipping when the controllers moved to keyboard
|
||||
rem and mouse. Earlier installs left one here -- remove it, but only if it is
|
||||
@@ -73,13 +70,10 @@ if exist "!GAME!\xinput9_1_0.dll" (
|
||||
echo WARNING: trlvr_launcher.exe not built; the game stays limited to 2 GB.
|
||||
)
|
||||
|
||||
rem OpenVR is loaded by name at runtime, so it has to sit beside the game.
|
||||
rem Without it the mod logs why and the game still runs, just flat.
|
||||
if exist "!ROOT!third_party\openvr\bin\win32\openvr_api.dll" (
|
||||
copy /y "!ROOT!third_party\openvr\bin\win32\openvr_api.dll" "!GAME!\openvr_api.dll" >nul
|
||||
) else (
|
||||
echo WARNING: openvr_api.dll ^(win32^) not found; VR will stay disabled.
|
||||
)
|
||||
rem Both VR runtimes' loaders sit beside the game (1.1.0): openxr_loader.dll
|
||||
rem (OpenXR, the default; delete it to fall back) and openvr_api.dll.
|
||||
copy /y "!ROOT!third_party\openxr\bin\win32\openxr_loader.dll" "!GAME!\openxr_loader.dll" >nul
|
||||
copy /y "!ROOT!third_party\openvr\bin\win32\openvr_api.dll" "!GAME!\openvr_api.dll" >nul
|
||||
|
||||
if /i "%~1"=="dxvk" (
|
||||
rem Our fork first -- it is the one with Direct3DCreateVR9, which the VR
|
||||
@@ -105,7 +99,8 @@ echo Installed into:
|
||||
echo !GAME!
|
||||
echo.
|
||||
echo d3d9.dll the proxy
|
||||
echo openvr_api.dll OpenVR runtime interface
|
||||
echo openxr_loader.dll OpenXR ^(the default; delete it to use OpenVR^)
|
||||
echo openvr_api.dll OpenVR / SteamVR ^(the fallback^)
|
||||
echo trlvr_launcher.exe starts the game with a 4 GB address space
|
||||
echo.
|
||||
echo Steam ^> Properties ^> Launch Options:
|
||||
@@ -121,6 +116,7 @@ exit /b 0
|
||||
del /q "!GAME!\d3d9.dll" 2>nul
|
||||
del /q "!GAME!\d3d9_dxvk.dll" 2>nul
|
||||
del /q "!GAME!\openvr_api.dll" 2>nul
|
||||
del /q "!GAME!\openxr_loader.dll" 2>nul
|
||||
del /q "!GAME!\trlvr_launcher.exe" 2>nul
|
||||
if exist "!GAME!\xinput9_1_0.dll" (
|
||||
findstr /m /c:"/actions/gameplay" "!GAME!\xinput9_1_0.dll" >nul 2>&1
|
||||
|
||||
@@ -10,6 +10,12 @@ cl /nologo /EHsc /MT /O2 /W4 /std:c++17 /D_CRT_SECURE_NO_WARNINGS ^
|
||||
/Fo"%OUT%\\" /Fe"%OUT%\mathtest.exe" ^
|
||||
"%ROOT%src\test\mathtest.cpp" "%ROOT%src\vr\vr_math.cpp" ^
|
||||
"%ROOT%src\vr\vr_gesture.cpp" ^
|
||||
"%ROOT%src\vr\physics\body_push.cpp" "%ROOT%src\vr\physics\body_volumes.cpp" ^
|
||||
"%ROOT%src\vr\godmode\god_carry.cpp" "%ROOT%src\vr\godmode\god_pluck.cpp" ^
|
||||
"%ROOT%src\vr\physics\soft_body.cpp" "%ROOT%src\vr\physics\rear_jiggle.cpp" ^
|
||||
"%ROOT%src\vr\physics\rear_shape.cpp" ^
|
||||
"%ROOT%src\vr\godmode\board_leash.cpp" "%ROOT%src\vr\menu_panel.cpp" ^
|
||||
"%ROOT%src\vr\finger_input.cpp" ^
|
||||
/link /MACHINE:X86
|
||||
if errorlevel 1 (
|
||||
echo BUILD FAILED
|
||||
|
||||
+6
-7
@@ -31,22 +31,19 @@ if exist "!DEST!\" (
|
||||
)
|
||||
)
|
||||
|
||||
for %%F in ("build\d3d9.dll" "dist\d3d9_dxvk.dll" "dist\trlvr_launcher.exe" "release\READ ME FIRST.txt" "input\actions.json") do (
|
||||
for %%F in ("build\d3d9.dll" "dist\d3d9_dxvk.dll" "dist\trlvr_launcher.exe" "release\READ ME FIRST.txt") do (
|
||||
if not exist "!ROOT!%%~F" (
|
||||
echo ERROR: !ROOT!%%~F is missing. Run build.bat first.
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
if not exist "!ROOT!third_party\openvr\bin\win32\openvr_api.dll" (
|
||||
echo ERROR: openvr_api.dll ^(win32^) not found in third_party\openvr.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist "!MAIN!" mkdir "!MAIN!"
|
||||
|
||||
copy /y "!ROOT!build\d3d9.dll" "!MAIN!\d3d9.dll" >nul
|
||||
copy /y "!ROOT!dist\d3d9_dxvk.dll" "!MAIN!\d3d9_dxvk.dll" >nul
|
||||
copy /y "!ROOT!dist\trlvr_launcher.exe" "!MAIN!\trlvr_launcher.exe" >nul
|
||||
rem Both VR runtimes (1.1.0): OpenXR by default, OpenVR as the fallback.
|
||||
copy /y "!ROOT!third_party\openxr\bin\win32\openxr_loader.dll" "!MAIN!\openxr_loader.dll" >nul
|
||||
copy /y "!ROOT!third_party\openvr\bin\win32\openvr_api.dll" "!MAIN!\openvr_api.dll" >nul
|
||||
copy /y "!ROOT!input\actions.json" "!MAIN!\trlvr_actions.json" >nul
|
||||
for %%F in ("!ROOT!input\bindings_*.json") do copy /y "%%~F" "!MAIN!\%%~nxF" >nul
|
||||
@@ -56,11 +53,13 @@ copy /y "!ROOT!release\VR CONTROLLER BINDINGS.txt" "!DEST!\VR CONTROLLER BI
|
||||
rem Licences: the mod's (Apache 2.0) and those of the binaries shipped with it
|
||||
rem (OpenVR's BSD licence asks for its notice beside the DLL).
|
||||
if not exist "!DEST!\Licences" mkdir "!DEST!\Licences"
|
||||
copy /y "!ROOT!third_party\openvr\LICENSE" "!DEST!\Licences\OpenVR-LICENSE.txt" >nul
|
||||
copy /y "!ROOT!LICENSE" "!DEST!\Licences\LICENSE.txt" >nul
|
||||
copy /y "!ROOT!NOTICE" "!DEST!\Licences\NOTICE.txt" >nul
|
||||
copy /y "!ROOT!THIRD_PARTY_NOTICES.md" "!DEST!\Licences\THIRD_PARTY_NOTICES.txt" >nul
|
||||
copy /y "!ROOT!dxvk-trlvr\LICENSE" "!DEST!\Licences\DXVK-LICENSE.txt" >nul
|
||||
copy /y "!ROOT!third_party\openvr\LICENSE" "!DEST!\Licences\OpenVR-LICENSE.txt" >nul
|
||||
copy /y "!ROOT!third_party\openxr\LICENSE" "!DEST!\Licences\OpenXR-SDK-LICENSE.txt" >nul
|
||||
copy /y "!ROOT!third_party\imgui\LICENSE.txt" "!DEST!\Licences\DearImGui-LICENSE.txt" >nul
|
||||
|
||||
rem Retired from the package. The XInput proxy stopped shipping when the
|
||||
rem controllers moved to keyboard and mouse -- see src/vr/vr_input.cpp.
|
||||
|
||||
+54
-27
@@ -1,5 +1,5 @@
|
||||
================================================================
|
||||
Tomb Raider: Legend -- VR mod VERSION 1.0.0
|
||||
Tomb Raider: Legend -- VR mod VERSION 1.1.0
|
||||
================================================================
|
||||
|
||||
Please report anything that goes wrong (see REPORTING PROBLEMS at the end)
|
||||
@@ -8,28 +8,40 @@ Please report anything that goes wrong (see REPORTING PROBLEMS at the end)
|
||||
WHAT YOU NEED
|
||||
-------------
|
||||
* Tomb Raider: Legend for PC -- the Steam or the GOG release
|
||||
* SteamVR and a PC VR headset with tracked controllers
|
||||
* A PC VR headset with tracked controllers, and SteamVR (or another
|
||||
OpenXR runtime that runs 32-bit games)
|
||||
(tested on HP Reverb G2; Oculus Touch, Index and WMR bindings included)
|
||||
|
||||
The original game files are not modified. The mod adds files beside trl.exe,
|
||||
and its launcher writes a separate patched copy named trl_vr.exe.
|
||||
|
||||
|
||||
NEW IN 1.0.0
|
||||
NEW IN 1.1.0
|
||||
------------
|
||||
* Soft body in third person too (immersive controls, body_soft = 1):
|
||||
her chest moves with her and gives under your hands.
|
||||
* Your whole hand meets Lara's body now -- palm, sides and fingers, not
|
||||
just the fingertip.
|
||||
* Player model mods work: the Lara Remastered Player Model Mod (and other
|
||||
replacement models with many more bones) no longer leave Lara
|
||||
invisible in first person.
|
||||
* TR7-Menu-Hook (the mod loader those mods need) shows its menu on a
|
||||
panel over your off hand. Point your main hand at it -- a laser shows
|
||||
where -- and pull the main trigger to click. While you point at the
|
||||
panel the game does not see that trigger. [vr] imgui_panel = 0 turns
|
||||
the panel off.
|
||||
* [developer] function_keys = 0 frees F1..F9 for other mods' menus.
|
||||
* OpenXR, with OpenVR as the fallback. The mod uses OpenXR by default and
|
||||
falls back to OpenVR / SteamVR if OpenXR cannot start. Delete
|
||||
openxr_loader.dll (or set vr_runtime = openvr) to always use OpenVR.
|
||||
The main menu shows the version and the runtime in use.
|
||||
* A settings panel in the headset: hold both sticks in for 0.6 s
|
||||
(Oculus Touch: hold the left menu button), or press Insert. Point at it
|
||||
with your main hand and pull the trigger, or use the sticks and A / B.
|
||||
Changes apply at once and are saved to trlvr.ini.
|
||||
* TR7-Menu-Hook's options are built into that panel when it is installed:
|
||||
refill health, next outfit, game speed, wireframe, hide UI, debug
|
||||
keypad and its saved settings.
|
||||
* God mode (board view): pinch or make a fist on Lara to pick her up,
|
||||
carry her and throw her by letting go mid-swing.
|
||||
* Body physics: her chest and rear give and bounce in third person, and a
|
||||
hand leaning on her in god mode pushes her body away.
|
||||
* Smooth glide when switching camera style.
|
||||
* Picture brightness and contrast for the headset (settings panel).
|
||||
* The palm sliders gain a third bar that moves the camera left or right
|
||||
(over either shoulder). Each camera style keeps its own size, zoom and
|
||||
side; a notch marks each default and the knob sticks there. Your index
|
||||
fingertip can now grab the knobs as well as your palm.
|
||||
* Movies are shown at their real 4:3 shape.
|
||||
* Smoother play with some player model mods.
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
1. PREPARE THE GAME
|
||||
@@ -76,11 +88,11 @@ STEAM: right-click the game > Properties > Launch Options and enter
|
||||
|
||||
"C:\Program Files (x86)\Steam\steamapps\common\Tomb Raider Legend\trlvr_launcher.exe" %command%
|
||||
|
||||
(adjust the path if the game is elsewhere), then start SteamVR and launch the
|
||||
game from Steam as usual.
|
||||
(adjust the path if the game is elsewhere), then start SteamVR (or your
|
||||
OpenXR runtime) and launch the game from Steam as usual.
|
||||
|
||||
GOG: start SteamVR, then run trlvr_launcher.exe from the game folder (make a
|
||||
shortcut to it if you like).
|
||||
GOG: start SteamVR (or your OpenXR runtime), then run trlvr_launcher.exe from
|
||||
the game folder (make a shortcut to it if you like).
|
||||
|
||||
The launcher makes trl_vr.exe, a large-address-aware copy of the game: this
|
||||
2006 32-bit game otherwise runs out of memory at headset resolution.
|
||||
@@ -156,9 +168,16 @@ Other camera options (third person, immersive controls on):
|
||||
|
||||
Optional body: first_person_body = full (whole body instead of hands only)
|
||||
|
||||
Third person extras: turn your off hand palm up for two sliders that scale
|
||||
the world and zoom the camera (saved per camera style). Walking beside a
|
||||
wall with nothing drawn, Lara runs her hand along it (wall_touch).
|
||||
Third person extras: turn your off hand palm up for three sliders -- the
|
||||
size of the world, the camera nearer or farther, and the camera left or
|
||||
right of Lara. Touch a knob with your other palm or index finger, grip and
|
||||
slide. Each camera style keeps its own; a notch marks each default. Walking
|
||||
beside a wall with nothing drawn, Lara runs her hand along it (wall_touch).
|
||||
|
||||
SETTINGS PANEL: hold both sticks in for 0.6 s (Oculus Touch: hold the left
|
||||
menu button), or press Insert on the keyboard. Cameras, picture, body
|
||||
physics, god mode, theatre, diorama and controls are all there, plus
|
||||
TR7-Menu-Hook's options when it is installed.
|
||||
|
||||
|
||||
------------------------------------------------------------
|
||||
@@ -171,6 +190,9 @@ VR CONTROLLERS (classic layout)
|
||||
(in menus: back)
|
||||
Left stick double click recenter the VR view
|
||||
Left stick long press pause (Esc)
|
||||
(Oculus Touch: the left menu button)
|
||||
Both sticks held in settings panel
|
||||
(Oculus Touch: hold the left menu button)
|
||||
Right stick turn
|
||||
Right stick click switch weapons
|
||||
Right stick double click open the PDA
|
||||
@@ -186,8 +208,9 @@ VR CONTROLLERS (classic layout)
|
||||
Y long press personal light
|
||||
|
||||
A long press is 0.65 seconds. VR CONTROLLER BINDINGS.txt has the per-headset
|
||||
details. If SteamVR keeps an old custom binding, open SteamVR > Settings >
|
||||
Controllers > Manage Controller Bindings and pick the supplied one.
|
||||
details. Under OpenVR, if SteamVR keeps an old custom binding, open SteamVR >
|
||||
Settings > Controllers > Manage Controller Bindings and pick the supplied one.
|
||||
Under OpenXR the bindings are built into the mod.
|
||||
|
||||
Keyboard: F1 recentres the view. The game's own keys still work.
|
||||
|
||||
@@ -223,7 +246,8 @@ SETTINGS WORTH KNOWING (trlvr.ini)
|
||||
Every setting has a short explanation in the file itself. Missing settings
|
||||
are added on start-up; values you chose are never replaced.
|
||||
|
||||
[vr] first_person, first_person_tracked_hands, first_person_body
|
||||
[vr] vr_runtime auto / openxr / openvr
|
||||
first_person, first_person_tracked_hands, first_person_body
|
||||
third_person_mode classic / shoulder / board / all
|
||||
comfort_vignette off / jumps / full (darkens the view edge)
|
||||
physical_crouch crouch for real to crouch
|
||||
@@ -236,6 +260,8 @@ are added on start-up; values you chose are never replaced.
|
||||
finger_tracking Lara's fingers follow yours (Quest, Index)
|
||||
first_person_hardcore the view turns with Lara through flips
|
||||
wall_touch third person: her hand along walls
|
||||
body_rear, body_push third person / god mode body physics
|
||||
picture_brightness, picture_contrast the headset image
|
||||
depth_of_field, fullscreen_effects, fsaa off / on / game
|
||||
[controls]
|
||||
immersive_controls gestures, holsters, hand climbing
|
||||
@@ -283,7 +309,8 @@ UNINSTALL
|
||||
Clear the Steam launch option (Steam) and remove these files from the game
|
||||
folder; GOG: rename D3D9.dll.gog-dxwrapper back to D3D9.dll.
|
||||
|
||||
d3d9.dll d3d9_dxvk.dll openvr_api.dll trlvr_launcher.exe
|
||||
d3d9.dll d3d9_dxvk.dll openxr_loader.dll openvr_api.dll
|
||||
trlvr_launcher.exe
|
||||
trlvr_actions.json bindings_*.json
|
||||
trlvr.ini trlvr.log* trlvr_crash.dmp* trl_vr.exe
|
||||
trlvr_launcher.log trl_vr_d3d9.log
|
||||
@@ -3,7 +3,17 @@
|
||||
================================================================
|
||||
|
||||
The VR controllers send the game's normal keyboard and mouse controls.
|
||||
The keyboard and mouse continue to work at the same time.
|
||||
The keyboard and mouse continue to work at the same time. The layout is the
|
||||
same under OpenXR (the default) and OpenVR (the fallback).
|
||||
|
||||
SETTINGS PANEL (1.1.0)
|
||||
----------------------
|
||||
Reverb G2, WMR, Index Hold both sticks in for 0.6 s
|
||||
Oculus Touch Hold the left menu button (a tap pauses)
|
||||
Keyboard Insert
|
||||
In the panel: point with your main hand and pull the trigger, or left stick
|
||||
moves, A selects, B goes back, right stick scrolls. The same gesture closes
|
||||
it.
|
||||
|
||||
|
||||
HP REVERB G2 / OCULUS TOUCH
|
||||
@@ -14,7 +24,7 @@ Left stick short click Gear menu: push the stick towards an item
|
||||
down switch weapon, left personal light);
|
||||
in menus: back (Escape)
|
||||
Left stick double click Recenter the VR view
|
||||
Left stick long click Pause (Escape)
|
||||
Left stick long click Pause (Escape); Oculus Touch: left menu button
|
||||
|
||||
Right stick Turn the camera (mouse movement)
|
||||
Right stick short click Switch weapons (End)
|
||||
|
||||
+369
-21
@@ -35,6 +35,18 @@ namespace trlvr
|
||||
|
||||
{ L"vr", L"enabled", L"1" },
|
||||
{ L"vr", L"submit", L"1" },
|
||||
{ L"vr", L"vr_runtime", L"auto" },
|
||||
{ L"vr", L"xr_runtime_json", L"" },
|
||||
{ L"vr", L"xr_copy_wait", L"1" },
|
||||
{ L"vr", L"xr_grip_match_openvr", L"1" },
|
||||
{ L"vr", L"view_transition_seconds", L"0.3" },
|
||||
{ L"vr", L"picture_brightness", L"0" },
|
||||
{ L"vr", L"picture_contrast", L"1" },
|
||||
{ L"vr", L"picture_follow_game", L"1" },
|
||||
{ L"vr", L"body_soft_sway_hz", L"3" },
|
||||
{ L"vr", L"body_soft_damping", L"0.53" },
|
||||
{ L"vr", L"body_soft_stretch", L"0.35" },
|
||||
{ L"vr", L"body_soft_ripple", L"0.3" },
|
||||
{ L"vr", L"stereo", L"1" },
|
||||
{ L"vr", L"stereo_same_frame", L"1" },
|
||||
{ L"vr", L"stereo_lock_pose", L"1" },
|
||||
@@ -73,10 +85,56 @@ namespace trlvr
|
||||
{ L"vr", L"board_distance", L"0.5" },
|
||||
{ L"vr", L"board_height", L"0.35" },
|
||||
{ L"vr", L"board_god_hand", L"1" },
|
||||
{ L"vr", L"god_head_zone", L"0.3" },
|
||||
{ L"vr", L"god_claim_zone", L"0.6" },
|
||||
{ L"vr", L"god_arm_zone", L"0.25" },
|
||||
{ L"vr", L"god_arm_lead", L"1" },
|
||||
{ L"vr", L"god_hang_arm", L"0" },
|
||||
{ L"vr", L"god_arms_limp", L"0" },
|
||||
{ L"vr", L"god_throw", L"1" },
|
||||
{ L"vr", L"god_throw_strength", L"1" },
|
||||
{ L"vr", L"god_throw_gravity", L"9.81" },
|
||||
{ L"vr", L"god_land_anim", L"-1" },
|
||||
{ L"vr", L"god_drop_height", L"1" },
|
||||
{ L"vr", L"god_grab_pose", L"1" },
|
||||
{ L"vr", L"god_drag_hand", L"0" },
|
||||
{ L"vr", L"body_push", L"1" },
|
||||
{ L"vr", L"body_push_strength", L"1" },
|
||||
{ L"vr", L"body_push_hz", L"2.5" },
|
||||
{ L"vr", L"body_push_damping", L"0.9" },
|
||||
{ L"vr", L"body_push_head_degrees", L"6" },
|
||||
{ L"vr", L"body_push_chest_degrees", L"4" },
|
||||
{ L"vr", L"body_push_waist_degrees", L"3" },
|
||||
{ L"vr", L"body_push_arm_degrees", L"8" },
|
||||
{ L"vr", L"body_push_hips_degrees", L"3" },
|
||||
{ L"vr", L"body_push_leg_degrees", L"2" },
|
||||
{ L"vr", L"body_push_soft", L"0" },
|
||||
{ L"vr", L"body_rear", L"1" },
|
||||
{ L"vr", L"body_rear_hz", L"4" },
|
||||
{ L"vr", L"body_rear_damping", L"0.47" },
|
||||
{ L"vr", L"body_rear_gain", L"0.55" },
|
||||
{ L"vr", L"body_rear_max_cm", L"3.7" },
|
||||
{ L"vr", L"body_rear_side", L"0.6" },
|
||||
{ L"vr", L"body_rear_back", L"1" },
|
||||
{ L"vr", L"body_rear_up", L"1" },
|
||||
{ L"vr", L"body_rear_fullness_cm", L"1.25" },
|
||||
{ L"vr", L"body_rear_limit_cm", L"1.5" },
|
||||
{ L"vr", L"body_rear_size", L"0.8" },
|
||||
{ L"vr", L"body_rear_width", L"1.21" },
|
||||
{ L"vr", L"body_rear_height", L"0.8" },
|
||||
{ L"vr", L"body_rear_spread_cm", L"4.6" },
|
||||
{ L"vr", L"body_rear_core", L"0.36" },
|
||||
{ L"vr", L"body_rear_lift_cm", L"-0.2" },
|
||||
{ L"vr", L"body_rear_inward", L"0.5" },
|
||||
{ L"vr", L"body_rear_touch_cm", L"0" },
|
||||
{ L"vr", L"body_rear_cleft_cm", L"0" },
|
||||
{ L"vr", L"body_rear_cleft_width_cm", L"2" },
|
||||
{ L"vr", L"body_rear_seam_cm", L"1" },
|
||||
{ L"vr", L"body_rear_debug", L"0" },
|
||||
{ L"vr", L"body_rear_test_cm", L"0" },
|
||||
{ L"vr", L"body_rear_dent", L"0.5" },
|
||||
{ L"vr", L"camera_collision", L"1" },
|
||||
{ L"vr", L"first_person_no_spread", L"1" },
|
||||
{ L"vr", L"mission_prep_backdrop_scale", L"1.15" },
|
||||
{ L"vr", L"mission_prep_backdrop_lag", L"0.4" },
|
||||
{ L"vr", L"weapon_recoil", L"1.0" },
|
||||
{ L"vr", L"weapon_haptics", L"1" },
|
||||
{ L"vr", L"shake_rumble", L"1" },
|
||||
@@ -94,7 +152,7 @@ namespace trlvr
|
||||
{ L"vr", L"body_spring", L"1" },
|
||||
{ L"vr", L"body_spring_angle", L"4" },
|
||||
{ L"vr", L"body_soft", L"1" },
|
||||
{ L"vr", L"body_soft_angle", L"12" },
|
||||
{ L"vr", L"body_soft_angle", L"14" },
|
||||
{ L"developer", L"function_keys", L"1" },
|
||||
{ L"vr", L"imgui_panel", L"1" },
|
||||
{ L"vr", L"cutscene_mode", L"normal" },
|
||||
@@ -121,8 +179,15 @@ namespace trlvr
|
||||
{ L"vr", L"first_person_smoothing", L"0.08" },
|
||||
{ L"vr", L"ui_passthrough", L"1" },
|
||||
{ L"vr", L"menu_world_locked", L"1" },
|
||||
{ L"vr", L"pause_world_locked", L"1" },
|
||||
{ L"vr", L"hide_gameplay_hud", L"0" },
|
||||
{ L"vr", L"ui_scale", L"0.6" },
|
||||
{ L"vr", L"hud_follow", L"0.85" },
|
||||
{ L"vr", L"overlay_vr_distance", L"1.0" },
|
||||
{ L"vr", L"overlay_vr_width_degrees", L"38" },
|
||||
{ L"vr", L"overlay_vr_yaw_degrees", L"15" },
|
||||
{ L"vr", L"overlay_vr_pitch_degrees", L"-10" },
|
||||
{ L"vr", L"overlay_vr_open_hold", L"0.6" },
|
||||
{ L"vr", L"cull_fov_degrees", L"120" },
|
||||
|
||||
{ L"controls", L"controllers", L"1" },
|
||||
@@ -191,6 +256,16 @@ namespace trlvr
|
||||
L"; 0 is the untouched flat-game baseline.\r\n"
|
||||
L"enabled = 1\r\n"
|
||||
L"submit = 1\r\n"
|
||||
L"; VR runtime: auto (OpenXR, or OpenVR / SteamVR if OpenXR cannot start\r\n"
|
||||
L"; or openxr_loader.dll is missing), openxr, or openvr.\r\n"
|
||||
L"vr_runtime = auto\r\n"
|
||||
L"; OpenXR runtime manifest (a 32-bit one). Empty uses the registered runtime.\r\n"
|
||||
L"xr_runtime_json =\r\n"
|
||||
L"; 1 waits for the GPU copy into the headset's image before handing it over.\r\n"
|
||||
L"xr_copy_wait = 1\r\n"
|
||||
L"; 1 = under OpenXR the hands sit where they do under OpenVR (the\r\n"
|
||||
L"; controllers' grip point converted; known for Reverb G2 and Touch).\r\n"
|
||||
L"xr_grip_match_openvr = 1\r\n"
|
||||
L"stereo = 1\r\n"
|
||||
L"; Both eyes in one game frame. Keep Depth of Field and Fullscreen Effects off.\r\n"
|
||||
L"stereo_same_frame = 1\r\n"
|
||||
@@ -274,16 +349,126 @@ namespace trlvr
|
||||
L"; her over (hold Y). Needs board mode, immersive controls and\r\n"
|
||||
L"; third person too.\r\n"
|
||||
L"board_god_hand = 1\r\n"
|
||||
L"; live: god mode (board view with board_god_hand), the pick-up zone round\r\n"
|
||||
L"; her head, metres at her size. 0.05 .. 1.\r\n"
|
||||
L"god_head_zone = 0.3\r\n"
|
||||
L"; live: god mode, a hand this close to her body (metres at her size)\r\n"
|
||||
L"; keeps its grip and trigger from the game. 0 .. 3.\r\n"
|
||||
L"god_claim_zone = 0.6\r\n"
|
||||
L"; live: god mode, a grip this close to one of her arms (metres at her\r\n"
|
||||
L"; size) holds that arm instead of her head. 0 = no arm grab. 0 .. 0.5.\r\n"
|
||||
L"god_arm_zone = 0.25\r\n"
|
||||
L"; live: god mode, holding her arm, only the arm follows your hand until\r\n"
|
||||
L"; you pull past her reach; then she hangs from that wrist. 0 = she is\r\n"
|
||||
L"; lifted by the arm at once.\r\n"
|
||||
L"god_arm_lead = 1\r\n"
|
||||
L"; live: god mode pick-up, 1 = Legend's old way: the dangle animation,\r\n"
|
||||
L"; hanging by her wrist. 0 = she hangs from where you pinched her.\r\n"
|
||||
L"god_hang_arm = 0\r\n"
|
||||
L"; live: god mode, 1 = her arms hang limp while she is held.\r\n"
|
||||
L"god_arms_limp = 0\r\n"
|
||||
L"; live: god mode, let go mid-swing and she flies off your hand, arcs and\r\n"
|
||||
L"; lands where she hits the level. 0 = always put down where you let go.\r\n"
|
||||
L"god_throw = 1\r\n"
|
||||
L"; live: god mode, a throw's speed: your hand's speed times this. 0.2 .. 3.\r\n"
|
||||
L"god_throw_strength = 1\r\n"
|
||||
L"; live: god mode, a thrown Lara's gravity, m/s^2 at your size. 1 .. 30.\r\n"
|
||||
L"god_throw_gravity = 9.81\r\n"
|
||||
L"; live: god mode, a thrown Lara landing plays this animation of hers (the\r\n"
|
||||
L"; game's own, by number). -1 = none. -1 .. 4000.\r\n"
|
||||
L"god_land_anim = -1\r\n"
|
||||
L"; live: god mode, let go over a floor and she falls from where you held\r\n"
|
||||
L"; her, but from no higher than this above the floor (metres at her size).\r\n"
|
||||
L"; 0 = set straight down on the floor. 0 .. 5.\r\n"
|
||||
L"god_drop_height = 1\r\n"
|
||||
L"; live: god mode, 1 = you grab with your hand's pose: a pinch (trigger\r\n"
|
||||
L"; and thumb down) or a fist (trigger and grip); the grip alone only\r\n"
|
||||
L"; points and presses. Needs finger_tracking. 0 = the grip button grabs.\r\n"
|
||||
L"god_grab_pose = 1\r\n"
|
||||
L"; live: EXPERIMENTAL, god mode: grab her hand or arm and drag her up\r\n"
|
||||
L"; like a ragdoll, hanging by IK. 0 = the head grab and Legend's dangle\r\n"
|
||||
L"; animation.\r\n"
|
||||
L"god_drag_hand = 0\r\n"
|
||||
L"; live: god mode, a hand leaning on her pushes her away: head, chest,\r\n"
|
||||
L"; waist, arms, hips and legs tilt a few degrees and come back. 0 = off.\r\n"
|
||||
L"body_push = 1\r\n"
|
||||
L"; live: how much any push tilts her. 0 .. 3.\r\n"
|
||||
L"body_push_strength = 1\r\n"
|
||||
L"; live: how quick the tilt is (spring Hz, 0.5 .. 6) and how damped\r\n"
|
||||
L"; (near 1 = no swing past rest; 0.2 .. 1.5).\r\n"
|
||||
L"body_push_hz = 2.5\r\n"
|
||||
L"body_push_damping = 0.9\r\n"
|
||||
L"; live: the most each part tilts, in degrees. Head 0 .. 20, chest and\r\n"
|
||||
L"; waist 0 .. 15, arms 0 .. 25, hips 0 .. 15, legs 0 .. 8.\r\n"
|
||||
L"body_push_head_degrees = 6\r\n"
|
||||
L"body_push_chest_degrees = 4\r\n"
|
||||
L"body_push_waist_degrees = 3\r\n"
|
||||
L"body_push_arm_degrees = 8\r\n"
|
||||
L"body_push_hips_degrees = 3\r\n"
|
||||
L"body_push_leg_degrees = 2\r\n"
|
||||
L"; live: a hand on her chest or rear pushes her whole body only this\r\n"
|
||||
L"; share as hard (the soft part gives instead). 0 .. 1.\r\n"
|
||||
L"body_push_soft = 0\r\n"
|
||||
L"; live: her buttocks: each cheek is a mass on a spring that lags and\r\n"
|
||||
L"; settles as she moves. 0 = off.\r\n"
|
||||
L"body_rear = 1\r\n"
|
||||
L"; live: how fast a cheek bounces, Hz (lower = softer, slower). 0.5 .. 8.\r\n"
|
||||
L"body_rear_hz = 4\r\n"
|
||||
L"; live: how quickly a bounce dies away (damping ratio). 0.05 .. 1.5.\r\n"
|
||||
L"body_rear_damping = 0.47\r\n"
|
||||
L"; live: how much of the cheeks' lag is shown. 0 = none. 0 .. 3.\r\n"
|
||||
L"body_rear_gain = 0.55\r\n"
|
||||
L"; live: the most a cheek can lag behind, cm at her size. 0 .. 8.\r\n"
|
||||
L"body_rear_max_cm = 3.7\r\n"
|
||||
L"; live: how much motion is kept along each of her axes: side to side,\r\n"
|
||||
L"; back and forth, up and down. 0 .. 2 each.\r\n"
|
||||
L"body_rear_side = 0.6\r\n"
|
||||
L"body_rear_back = 1\r\n"
|
||||
L"body_rear_up = 1\r\n"
|
||||
L"; live: baseline fullness, cm at her size: how far the cheeks stand out\r\n"
|
||||
L"; at rest (minus = slimmer). The other sizes only change the region\r\n"
|
||||
L"; that moves. -2 .. 5.\r\n"
|
||||
L"body_rear_fullness_cm = 1.25\r\n"
|
||||
L"; live: the absolute most any point of the cheeks moves from rest, cm at\r\n"
|
||||
L"; her size, whatever the gain or a hand's pull: eased toward it\r\n"
|
||||
L"; (tanh), never stopping dead at it. 0.5 .. 8.\r\n"
|
||||
L"body_rear_limit_cm = 1.5\r\n"
|
||||
L"; live: size of the moved region round each cheek. 0.5 .. 2.\r\n"
|
||||
L"body_rear_size = 0.8\r\n"
|
||||
L"; live: its width and height, multiples of the size. 0.5 .. 2.\r\n"
|
||||
L"body_rear_width = 1.21\r\n"
|
||||
L"body_rear_height = 0.8\r\n"
|
||||
L"; live: the regions are fitted to her mesh; 8 = as fitted, more moves each\r\n"
|
||||
L"; one further out from her middle by (value - 8) cm. 0 .. 16.\r\n"
|
||||
L"body_rear_spread_cm = 4.6\r\n"
|
||||
L"; live: how much of the region moves fully before easing out. 0 .. 0.9.\r\n"
|
||||
L"body_rear_core = 0.36\r\n"
|
||||
L"; live: moves both regions up (+) or down (-), cm. -12 .. 12.\r\n"
|
||||
L"body_rear_lift_cm = -0.2\r\n"
|
||||
L"; live: how much motion INTO her body is kept. 0 .. 1.\r\n"
|
||||
L"body_rear_inward = 0.5\r\n"
|
||||
L"; live: god mode: grows (+) or shrinks (-) each glute's touch zone, cm.\r\n"
|
||||
L"; -3 .. 3.\r\n"
|
||||
L"body_rear_touch_cm = 0\r\n"
|
||||
L"; live: pulls the cleft between the glutes into her body, cm (0 .. 3),\r\n"
|
||||
L"; over this many cm either side (0.5 .. 6).\r\n"
|
||||
L"body_rear_cleft_cm = 0\r\n"
|
||||
L"body_rear_cleft_width_cm = 2\r\n"
|
||||
L"; live: the cut at her midline, cm; 0 = no cut. 0 .. 4.\r\n"
|
||||
L"body_rear_seam_cm = 1\r\n"
|
||||
L"; live: tuning aid: draws each cheek's region as lines. 1 = on.\r\n"
|
||||
L"body_rear_debug = 0\r\n"
|
||||
L"; live: tuning aid: pushes both cheeks this many cm back. -5 .. 5.\r\n"
|
||||
L"body_rear_test_cm = 0\r\n"
|
||||
L"; live: god mode: your fingertip dents her buttocks; the dent's size as a\r\n"
|
||||
L"; multiple of a fingertip, 0 = no dent. 0 .. 2.\r\n"
|
||||
L"body_rear_dent = 0.5\r\n"
|
||||
L"; Third-person camera collision: the view stops short of rock\r\n"
|
||||
L"; and walls instead of passing into them (0 = allow clipping).\r\n"
|
||||
L"camera_collision = 1\r\n"
|
||||
L"; First person: no random bullet spread -- shots go exactly where\r\n"
|
||||
L"; the gun points (0 = keep the game's spread).\r\n"
|
||||
L"first_person_no_spread = 1\r\n"
|
||||
L"; Backdrop behind the Mission Prep menu: size relative to the view\r\n"
|
||||
L"; (1 = exactly fills it) and how slowly it follows the head (s).\r\n"
|
||||
L"mission_prep_backdrop_scale = 1.15\r\n"
|
||||
L"mission_prep_backdrop_lag = 0.4\r\n"
|
||||
L"; Visual gun kick per shot: 0 = off, 1 = default, up to 2.\r\n"
|
||||
L"weapon_recoil = 1.0\r\n"
|
||||
L"; 1 = the firing controller vibrates on each shot.\r\n"
|
||||
@@ -328,7 +513,7 @@ namespace trlvr
|
||||
L"; 1 = her soft parts bounce (mostly up and down) and give under\r\n"
|
||||
L"; your hands; at most body_soft_angle degrees (0..30).\r\n"
|
||||
L"body_soft = 1\r\n"
|
||||
L"body_soft_angle = 12\r\n"
|
||||
L"body_soft_angle = 14\r\n"
|
||||
L"; 1 = other mods' ImGui menus (TR7-Menu-Hook) are shown on a\r\n"
|
||||
L"; panel over your off hand; point the main hand at it and pull\r\n"
|
||||
L"; its trigger to click.\r\n"
|
||||
@@ -371,10 +556,27 @@ namespace trlvr
|
||||
L"ui_passthrough = 1\r\n"
|
||||
L"; Menus stay where they opened; the gameplay HUD follows the headset.\r\n"
|
||||
L"menu_world_locked = 1\r\n"
|
||||
L"; 1 = the pause menu stays level in the room where you were looking\r\n"
|
||||
L"; when it opened; 0 = it follows the headset (head-locked).\r\n"
|
||||
L"pause_world_locked = 1\r\n"
|
||||
L"; 1 = hide the gameplay HUD completely (world markers stay).\r\n"
|
||||
L"hide_gameplay_hud = 0\r\n"
|
||||
L"ui_scale = 0.6\r\n"
|
||||
L"; 1.0 is rigidly head-locked; 0.85 gives the gameplay HUD gentle drag.\r\n"
|
||||
L"hud_follow = 0.85\r\n"
|
||||
L"\r\n"
|
||||
L"; The settings window (Insert) in the headset: a panel placed in front\r\n"
|
||||
L"; of you when it opens. Distance in metres (0.3 to 5), width in degrees\r\n"
|
||||
L"; (10 to 120), yaw degrees to the left (-90 to 90, negative = right),\r\n"
|
||||
L"; pitch degrees up (-60 to 60, negative = down).\r\n"
|
||||
L"overlay_vr_distance = 1.0\r\n"
|
||||
L"overlay_vr_width_degrees = 38\r\n"
|
||||
L"overlay_vr_yaw_degrees = 15\r\n"
|
||||
L"overlay_vr_pitch_degrees = -10\r\n"
|
||||
L"; Hold the controller menu (pause) button this many seconds to open\r\n"
|
||||
L"; it; a quicker press still pauses the game. 0 = off (0 to 3).\r\n"
|
||||
L"overlay_vr_open_hold = 0.6\r\n"
|
||||
L"\r\n"
|
||||
L"; Object visibility only. Room culling is conservatively bypassed for VR.\r\n"
|
||||
L"cull_fov_degrees = 120\r\n"
|
||||
L"; Draw the world at true proportions (removes the game's flat-screen\r\n"
|
||||
@@ -562,6 +764,17 @@ namespace trlvr
|
||||
}
|
||||
}
|
||||
|
||||
Config& config_edit()
|
||||
{
|
||||
return g_config;
|
||||
}
|
||||
|
||||
void config_save_setting(const wchar_t* section, const wchar_t* key,
|
||||
const wchar_t* value)
|
||||
{
|
||||
save_setting(section, key, value);
|
||||
}
|
||||
|
||||
void config_set_theatre_size(float width, float height, bool save)
|
||||
{
|
||||
g_config.cutscene_screen_width = width;
|
||||
@@ -671,6 +884,18 @@ namespace trlvr
|
||||
|
||||
g_config.vr_enabled = read_bool(L"vr", L"enabled", true, path);
|
||||
g_config.vr_submit = read_bool(L"vr", L"submit", true, path);
|
||||
{
|
||||
wchar_t text[16]{};
|
||||
GetPrivateProfileStringW(L"vr", L"vr_runtime", L"auto", text, 16,
|
||||
path);
|
||||
g_config.vr_runtime = _wcsicmp(text, L"openxr") == 0 ? 1
|
||||
: _wcsicmp(text, L"openvr") == 0 ? 2 : 0;
|
||||
}
|
||||
GetPrivateProfileStringW(L"vr", L"xr_runtime_json", L"",
|
||||
g_config.xr_runtime_json, MAX_PATH, path);
|
||||
g_config.xr_copy_wait = read_bool(L"vr", L"xr_copy_wait", true, path);
|
||||
g_config.xr_grip_match_openvr =
|
||||
read_bool(L"vr", L"xr_grip_match_openvr", true, path);
|
||||
g_config.stereo = read_bool(L"vr", L"stereo", true, path);
|
||||
g_config.stereo_same_frame = read_bool(L"vr", L"stereo_same_frame", true, path);
|
||||
g_config.stereo_lock_pose = read_bool(L"vr", L"stereo_lock_pose", true, path);
|
||||
@@ -746,17 +971,6 @@ namespace trlvr
|
||||
_wcsnicmp(culling, L"on", 2) == 0 ? 1
|
||||
: _wcsnicmp(culling, L"game", 4) == 0 ? -1 : 0;
|
||||
}
|
||||
{
|
||||
const float backdrop = read_float(
|
||||
L"vr", L"mission_prep_backdrop_scale", L"1.15", path);
|
||||
g_config.mission_prep_backdrop_scale =
|
||||
std::isfinite(backdrop) && backdrop >= 0.3f &&
|
||||
backdrop <= 6.0f ? backdrop : 1.15f;
|
||||
const float lag = read_float(
|
||||
L"vr", L"mission_prep_backdrop_lag", L"0.4", path);
|
||||
g_config.mission_prep_backdrop_lag =
|
||||
std::isfinite(lag) && lag >= 0.0f && lag <= 3.0f ? lag : 0.4f;
|
||||
}
|
||||
{
|
||||
wchar_t vignette[32] = {};
|
||||
GetPrivateProfileStringW(L"vr", L"comfort_vignette", L"off",
|
||||
@@ -815,9 +1029,9 @@ namespace trlvr
|
||||
read_bool(L"vr", L"hand_body_collision", true, path);
|
||||
g_config.body_spring = read_bool(L"vr", L"body_spring", true, path);
|
||||
g_config.body_soft = read_bool(L"vr", L"body_soft", true, path);
|
||||
value = read_float(L"vr", L"body_soft_angle", L"12", path);
|
||||
value = read_float(L"vr", L"body_soft_angle", L"14", path);
|
||||
g_config.body_soft_angle = std::isfinite(value)
|
||||
? (value < 0.0f ? 0.0f : value > 30.0f ? 30.0f : value) : 12.0f;
|
||||
? (value < 0.0f ? 0.0f : value > 30.0f ? 30.0f : value) : 14.0f;
|
||||
// Undocumented (not written to the INI): [vr] body_soft_scale.
|
||||
value = read_float(L"vr", L"body_soft_scale", L"1", path);
|
||||
g_config.body_soft_scale = std::isfinite(value) && value >= 0.5f &&
|
||||
@@ -825,6 +1039,114 @@ namespace trlvr
|
||||
g_config.function_keys = read_bool(L"developer", L"function_keys",
|
||||
true, path);
|
||||
g_config.imgui_panel = read_bool(L"vr", L"imgui_panel", true, path);
|
||||
value = read_float(L"vr", L"body_soft_sway_hz", L"3", path);
|
||||
g_config.body_soft_sway_hz = std::isfinite(value)
|
||||
? (value < 0.8f ? 0.8f : value > 4.0f ? 4.0f : value) : 3.0f;
|
||||
value = read_float(L"vr", L"body_soft_damping", L"0.53", path);
|
||||
g_config.body_soft_damping = std::isfinite(value)
|
||||
? (value < 0.03f ? 0.03f : value > 0.6f ? 0.6f : value) : 0.53f;
|
||||
value = read_float(L"vr", L"picture_brightness", L"0", path);
|
||||
g_config.picture_brightness = std::isfinite(value)
|
||||
? (value < -0.5f ? -0.5f : value > 0.5f ? 0.5f : value) : 0.0f;
|
||||
value = read_float(L"vr", L"picture_contrast", L"1", path);
|
||||
g_config.picture_contrast = std::isfinite(value)
|
||||
? (value < 0.5f ? 0.5f : value > 2.0f ? 2.0f : value) : 1.0f;
|
||||
g_config.picture_follow_game =
|
||||
read_bool(L"vr", L"picture_follow_game", true, path);
|
||||
value = read_float(L"vr", L"body_soft_stretch", L"0.35", path);
|
||||
g_config.body_soft_stretch = std::isfinite(value)
|
||||
? (value < 0.0f ? 0.0f : value > 1.0f ? 1.0f : value) : 0.35f;
|
||||
value = read_float(L"vr", L"body_soft_ripple", L"0.3", path);
|
||||
g_config.body_soft_ripple = std::isfinite(value)
|
||||
? (value < 0.0f ? 0.0f : value > 1.0f ? 1.0f : value) : 0.3f;
|
||||
{
|
||||
auto rf = [&](const wchar_t* key, const wchar_t* def, float lo,
|
||||
float hi, float fallback) {
|
||||
const float v = read_float(L"vr", key, def, path);
|
||||
return std::isfinite(v) ? (v < lo ? lo : v > hi ? hi : v)
|
||||
: fallback;
|
||||
};
|
||||
auto rb = [&](const wchar_t* key, bool fallback) {
|
||||
return read_bool(L"vr", key, fallback, path);
|
||||
};
|
||||
Config& c = g_config;
|
||||
c.god_head_zone = rf(L"god_head_zone", L"0.3", 0.05f, 1.0f, 0.3f);
|
||||
c.god_claim_zone = rf(L"god_claim_zone", L"0.6", 0.0f, 3.0f, 0.6f);
|
||||
c.god_arm_zone = rf(L"god_arm_zone", L"0.25", 0.0f, 0.5f, 0.25f);
|
||||
c.god_arm_lead = rb(L"god_arm_lead", true);
|
||||
c.god_hang_arm = rb(L"god_hang_arm", false);
|
||||
c.god_arms_limp = rb(L"god_arms_limp", false);
|
||||
c.god_throw = rb(L"god_throw", true);
|
||||
c.god_throw_strength =
|
||||
rf(L"god_throw_strength", L"1", 0.2f, 3.0f, 1.0f);
|
||||
c.god_throw_gravity =
|
||||
rf(L"god_throw_gravity", L"9.81", 1.0f, 30.0f, 9.81f);
|
||||
c.god_land_anim = (int)rf(L"god_land_anim", L"-1", -1.0f,
|
||||
4000.0f, -1.0f);
|
||||
c.god_drop_height =
|
||||
rf(L"god_drop_height", L"1", 0.0f, 5.0f, 1.0f);
|
||||
c.god_grab_pose = rb(L"god_grab_pose", true);
|
||||
c.god_drag_hand = rb(L"god_drag_hand", false);
|
||||
c.body_push = rb(L"body_push", true);
|
||||
c.body_push_strength =
|
||||
rf(L"body_push_strength", L"1", 0.0f, 3.0f, 1.0f);
|
||||
c.body_push_hz = rf(L"body_push_hz", L"2.5", 0.5f, 6.0f, 2.5f);
|
||||
c.body_push_damping =
|
||||
rf(L"body_push_damping", L"0.9", 0.2f, 1.5f, 0.9f);
|
||||
c.body_push_head_degrees =
|
||||
rf(L"body_push_head_degrees", L"6", 0.0f, 20.0f, 6.0f);
|
||||
c.body_push_chest_degrees =
|
||||
rf(L"body_push_chest_degrees", L"4", 0.0f, 15.0f, 4.0f);
|
||||
c.body_push_waist_degrees =
|
||||
rf(L"body_push_waist_degrees", L"3", 0.0f, 15.0f, 3.0f);
|
||||
c.body_push_arm_degrees =
|
||||
rf(L"body_push_arm_degrees", L"8", 0.0f, 25.0f, 8.0f);
|
||||
c.body_push_hips_degrees =
|
||||
rf(L"body_push_hips_degrees", L"3", 0.0f, 15.0f, 3.0f);
|
||||
c.body_push_leg_degrees =
|
||||
rf(L"body_push_leg_degrees", L"2", 0.0f, 8.0f, 2.0f);
|
||||
c.body_push_soft = rf(L"body_push_soft", L"0", 0.0f, 1.0f, 0.0f);
|
||||
c.body_rear = rb(L"body_rear", true);
|
||||
c.body_rear_hz = rf(L"body_rear_hz", L"4", 0.5f, 8.0f, 4.0f);
|
||||
c.body_rear_damping =
|
||||
rf(L"body_rear_damping", L"0.47", 0.05f, 1.5f, 0.47f);
|
||||
c.body_rear_gain = rf(L"body_rear_gain", L"0.55", 0.0f, 3.0f, 0.55f);
|
||||
c.body_rear_max_cm =
|
||||
rf(L"body_rear_max_cm", L"3.7", 0.0f, 8.0f, 3.7f);
|
||||
c.body_rear_side = rf(L"body_rear_side", L"0.6", 0.0f, 2.0f, 0.6f);
|
||||
c.body_rear_back = rf(L"body_rear_back", L"1", 0.0f, 2.0f, 1.0f);
|
||||
c.body_rear_up = rf(L"body_rear_up", L"1", 0.0f, 2.0f, 1.0f);
|
||||
c.body_rear_fullness_cm =
|
||||
rf(L"body_rear_fullness_cm", L"1.25", -2.0f, 5.0f, 1.25f);
|
||||
c.body_rear_limit_cm =
|
||||
rf(L"body_rear_limit_cm", L"1.5", 0.5f, 8.0f, 1.5f);
|
||||
c.body_rear_size = rf(L"body_rear_size", L"0.8", 0.5f, 2.0f, 0.8f);
|
||||
c.body_rear_width = rf(L"body_rear_width", L"1.21", 0.5f, 2.0f, 1.21f);
|
||||
c.body_rear_height =
|
||||
rf(L"body_rear_height", L"0.8", 0.5f, 2.0f, 0.8f);
|
||||
c.body_rear_spread_cm =
|
||||
rf(L"body_rear_spread_cm", L"4.6", 0.0f, 16.0f, 4.6f);
|
||||
c.body_rear_core = rf(L"body_rear_core", L"0.36", 0.0f, 0.9f, 0.36f);
|
||||
c.body_rear_lift_cm =
|
||||
rf(L"body_rear_lift_cm", L"-0.2", -12.0f, 12.0f, -0.2f);
|
||||
c.body_rear_inward =
|
||||
rf(L"body_rear_inward", L"0.5", 0.0f, 1.0f, 0.5f);
|
||||
c.body_rear_touch_cm =
|
||||
rf(L"body_rear_touch_cm", L"0", -3.0f, 3.0f, 0.0f);
|
||||
c.body_rear_cleft_cm =
|
||||
rf(L"body_rear_cleft_cm", L"0", 0.0f, 3.0f, 0.0f);
|
||||
c.body_rear_cleft_width_cm =
|
||||
rf(L"body_rear_cleft_width_cm", L"2", 0.5f, 6.0f, 2.0f);
|
||||
c.body_rear_seam_cm =
|
||||
rf(L"body_rear_seam_cm", L"1", 0.0f, 4.0f, 1.0f);
|
||||
c.body_rear_debug = rb(L"body_rear_debug", false);
|
||||
c.body_rear_test_cm =
|
||||
rf(L"body_rear_test_cm", L"0", -5.0f, 5.0f, 0.0f);
|
||||
c.body_rear_dent = rf(L"body_rear_dent", L"0.5", 0.0f, 2.0f, 0.5f);
|
||||
}
|
||||
value = read_float(L"vr", L"view_transition_seconds", L"0.3", path);
|
||||
g_config.view_transition_seconds = std::isfinite(value)
|
||||
? (value < 0.0f ? 0.0f : value > 2.0f ? 2.0f : value) : 0.3f;
|
||||
value = read_float(L"vr", L"body_spring_angle", L"4", path);
|
||||
g_config.body_spring_angle = std::isfinite(value)
|
||||
? (value < 0.0f ? 0.0f : value > 15.0f ? 15.0f : value) : 4.0f;
|
||||
@@ -916,10 +1238,36 @@ namespace trlvr
|
||||
value >= 0.0f && value <= 1.0f ? value : 0.08f;
|
||||
g_config.ui_passthrough = read_bool(L"vr", L"ui_passthrough", true, path);
|
||||
g_config.menu_world_locked = read_bool(L"vr", L"menu_world_locked", true, path);
|
||||
g_config.pause_world_locked =
|
||||
read_bool(L"vr", L"pause_world_locked", true, path);
|
||||
g_config.hide_gameplay_hud =
|
||||
read_bool(L"vr", L"hide_gameplay_hud", false, path);
|
||||
value = read_float(L"vr", L"ui_scale", L"0.6", path);
|
||||
g_config.ui_scale = value >= 0.1f && value <= 2.0f ? value : 0.6f;
|
||||
value = read_float(L"vr", L"hud_follow", L"0.85", path);
|
||||
g_config.hud_follow = value >= 0.0f && value <= 1.0f ? value : 0.85f;
|
||||
{
|
||||
const auto clamped = [](float v, float low, float high,
|
||||
float fallback) {
|
||||
return std::isfinite(v) ? (v < low ? low : v > high ? high : v)
|
||||
: fallback;
|
||||
};
|
||||
g_config.overlay_vr_distance = clamped(
|
||||
read_float(L"vr", L"overlay_vr_distance", L"1.0", path),
|
||||
0.3f, 5.0f, 1.0f);
|
||||
g_config.overlay_vr_width_degrees = clamped(
|
||||
read_float(L"vr", L"overlay_vr_width_degrees", L"38", path),
|
||||
10.0f, 120.0f, 38.0f);
|
||||
g_config.overlay_vr_yaw_degrees = clamped(
|
||||
read_float(L"vr", L"overlay_vr_yaw_degrees", L"15", path),
|
||||
-90.0f, 90.0f, 15.0f);
|
||||
g_config.overlay_vr_pitch_degrees = clamped(
|
||||
read_float(L"vr", L"overlay_vr_pitch_degrees", L"-10", path),
|
||||
-60.0f, 60.0f, -10.0f);
|
||||
g_config.overlay_vr_open_hold = clamped(
|
||||
read_float(L"vr", L"overlay_vr_open_hold", L"0.6", path),
|
||||
0.0f, 3.0f, 0.6f);
|
||||
}
|
||||
value = read_float(L"vr", L"cull_fov_degrees", L"120", path);
|
||||
g_config.cull_fov_degrees =
|
||||
value >= 100.0f && value <= 150.0f ? value : 120.0f;
|
||||
|
||||
+137
-5
@@ -17,6 +17,19 @@ namespace trlvr
|
||||
|
||||
bool vr_enabled = true;
|
||||
bool vr_submit = true;
|
||||
// OpenXR: a runtime manifest to use instead of the registered
|
||||
// 32-bit one (XR_RUNTIME_JSON), and whether each frame's copy
|
||||
// into the runtime's swapchain is waited for before it is handed
|
||||
// over.
|
||||
// VR backend (1.1.0): 0 auto (OpenXR, falling back to OpenVR when
|
||||
// openxr_loader.dll is missing or no OpenXR runtime starts),
|
||||
// 1 OpenXR only, 2 OpenVR / SteamVR only.
|
||||
int vr_runtime = 0;
|
||||
wchar_t xr_runtime_json[MAX_PATH] = L"";
|
||||
bool xr_copy_wait = true;
|
||||
// OpenXR: convert the controllers' grip pose to the OpenVR grip pose
|
||||
// the hand tuning was made against (known for Reverb G2 and Touch).
|
||||
bool xr_grip_match_openvr = true;
|
||||
bool stereo = true;
|
||||
bool stereo_same_frame = true;
|
||||
bool stereo_lock_pose = true;
|
||||
@@ -77,13 +90,24 @@ namespace trlvr
|
||||
// mostly up and down, and give under the hands; at most
|
||||
// body_soft_angle degrees.
|
||||
bool body_soft = true;
|
||||
float body_soft_angle = 12.0f;
|
||||
float body_soft_angle = 14.0f;
|
||||
// Undocumented: size of the soft parts (1 = as modelled).
|
||||
float body_soft_scale = 1.0f;
|
||||
// F1..F9 developer keys (0 frees them for other mods' menus).
|
||||
bool function_keys = true;
|
||||
// Other mods' Dear ImGui menus shown as a VR panel.
|
||||
bool imgui_panel = true;
|
||||
// Third person (2026-10-14): how loose the soft parts are sideways
|
||||
// -- their side-to-side frequency in Hz (lower swings wider and
|
||||
// slower; up and down stays at 2.2) -- and how quickly a swing
|
||||
// dies away (damping ratio; lower rings longer).
|
||||
float body_soft_sway_hz = 3.0f;
|
||||
float body_soft_damping = 0.53f;
|
||||
// Soft parts' shape (2026-10-14): squash and stretch along the bone
|
||||
// (0 none .. 1 strong, volume kept) and how much of a lighter second
|
||||
// mass's overrun -- the ripple on top of the swing -- is shown.
|
||||
float body_soft_stretch = 0.35f;
|
||||
float body_soft_ripple = 0.3f;
|
||||
bool hmd_aim = true;
|
||||
float world_scale = 291.0f;
|
||||
float head_rotation_scale = 1.0f;
|
||||
@@ -144,6 +168,16 @@ namespace trlvr
|
||||
// cycles classic -> shoulder -> board -> first person (2026-10-06).
|
||||
bool view_cycle_all = true;
|
||||
float third_person_smoothing = 0.12f; // seconds
|
||||
// Seconds a view switch takes to glide from one view to the next
|
||||
// (position and world scale); 0 cuts.
|
||||
float view_transition_seconds = 0.3f;
|
||||
// Picture (2026-10-14): brightness (-0.5 .. 0.5, added) and contrast
|
||||
// (0.5 .. 2, about mid grey) on the frame sent to the headset, and
|
||||
// whether the game's own brightness / contrast sliders (its gamma
|
||||
// ramp, ignored by Windows in a window) are applied too.
|
||||
float picture_brightness = 0.0f;
|
||||
float picture_contrast = 1.0f;
|
||||
bool picture_follow_game = true;
|
||||
float shoulder_distance = 1.3f; // metres behind the focus
|
||||
float shoulder_height = 0.0f; // metres above it
|
||||
float shoulder_side = 0.35f; // metres to the right
|
||||
@@ -155,16 +189,90 @@ namespace trlvr
|
||||
// flick). Only with third_person_mode = board, immersive_controls
|
||||
// = 1 and first_person = 0 as well.
|
||||
bool board_god_hand = true;
|
||||
// God-hand pick-up (board mode with board_god_hand only). Zones are
|
||||
// metres at her size: head grip zone, claim zone (a hand this close
|
||||
// keeps its grip/trigger from the game) and arm grip zone (0 = no
|
||||
// arm grab). god_arm_lead: a held arm leads until pulled past her
|
||||
// reach. god_hang_arm: Legend's old dangle by the wrist. god_throw:
|
||||
// letting go mid-swing throws her (speed x strength, own gravity);
|
||||
// god_land_anim plays that animation on landing (-1 none);
|
||||
// god_drop_height caps the fall when put down over a floor.
|
||||
float god_head_zone = 0.3f;
|
||||
float god_claim_zone = 0.6f;
|
||||
float god_arm_zone = 0.25f;
|
||||
bool god_arm_lead = true;
|
||||
bool god_hang_arm = false;
|
||||
bool god_arms_limp = false;
|
||||
bool god_throw = true;
|
||||
float god_throw_strength = 1.0f;
|
||||
float god_throw_gravity = 9.81f;
|
||||
int god_land_anim = -1;
|
||||
float god_drop_height = 1.0f;
|
||||
// god_grab_pose: grabs come from the hand's pose -- a pinch (trigger
|
||||
// + thumb down) or a fist (trigger + grip); grip alone only points
|
||||
// and presses. 0 = the old grip-button grabs.
|
||||
bool god_grab_pose = true;
|
||||
// Experimental: grab her hand or arm and drag her up like a
|
||||
// ragdoll (arm zones, procedural IK hang). Off: the head grab with
|
||||
// Legend's own dangle animation.
|
||||
bool god_drag_hand = false;
|
||||
// Whole-body push (god mode): a hand leaning on her tilts head,
|
||||
// chest, waist, arms, hips and legs by up to these degrees on a
|
||||
// spring (hz, damping), scaled by body_push_strength.
|
||||
bool body_push = true;
|
||||
float body_push_strength = 1.0f;
|
||||
float body_push_hz = 2.5f;
|
||||
float body_push_damping = 0.9f;
|
||||
float body_push_head_degrees = 6.0f;
|
||||
float body_push_chest_degrees = 4.0f;
|
||||
float body_push_waist_degrees = 3.0f;
|
||||
float body_push_arm_degrees = 8.0f;
|
||||
float body_push_hips_degrees = 3.0f;
|
||||
float body_push_leg_degrees = 2.0f;
|
||||
// A hand on a soft part (chest or rear) pushes her whole body only
|
||||
// this share as hard as one on the rest of her (0 = not at all): the
|
||||
// soft part gives, the body barely does.
|
||||
float body_push_soft = 0.0f;
|
||||
// Rear soft body: the buttocks as masses on springs (hz, damping,
|
||||
// gain, max_cm of lag), the share of motion kept per axis, the
|
||||
// region's size/shape/placement, the dent a fingertip makes, and two
|
||||
// tuning aids (debug lines, test push).
|
||||
bool body_rear = true;
|
||||
float body_rear_hz = 4.0f;
|
||||
float body_rear_damping = 0.47f;
|
||||
float body_rear_gain = 0.55f;
|
||||
float body_rear_max_cm = 3.7f;
|
||||
float body_rear_side = 0.6f;
|
||||
float body_rear_back = 1.0f;
|
||||
float body_rear_up = 1.0f;
|
||||
// Baseline fullness (cm at her size, negative = slimmer): how far
|
||||
// the cheeks stand out at rest, along the same falloff. The size,
|
||||
// width and height only change the region that moves.
|
||||
float body_rear_fullness_cm = 1.25f;
|
||||
// The absolute most any point of the cheeks moves from rest, cm at
|
||||
// her size, whatever the gain or a hand's pull (limit x tanh(move /
|
||||
// limit): eased toward it all the way, never stopping dead at it).
|
||||
float body_rear_limit_cm = 1.5f;
|
||||
float body_rear_size = 0.8f;
|
||||
float body_rear_width = 1.21f;
|
||||
float body_rear_height = 0.8f;
|
||||
float body_rear_spread_cm = 4.6f;
|
||||
float body_rear_core = 0.36f;
|
||||
float body_rear_lift_cm = -0.2f;
|
||||
float body_rear_inward = 0.5f;
|
||||
float body_rear_touch_cm = 0.0f;
|
||||
float body_rear_cleft_cm = 0.0f;
|
||||
float body_rear_cleft_width_cm = 2.0f;
|
||||
float body_rear_seam_cm = 1.0f;
|
||||
bool body_rear_debug = false;
|
||||
float body_rear_test_cm = 0.0f;
|
||||
float body_rear_dent = 0.5f;
|
||||
// Third-person camera collision: the board seat and the head-moved
|
||||
// eye stop short of geometry (0 = allow clipping into it).
|
||||
bool camera_collision = true;
|
||||
// First person: bullets go exactly where the gun points (0 = keep
|
||||
// the game's random spread).
|
||||
bool first_person_no_spread = true;
|
||||
// Mission Prep backdrop: size relative to the view (1 = exactly
|
||||
// fills it) and how slowly it turns after the head (seconds).
|
||||
float mission_prep_backdrop_scale = 1.15f;
|
||||
float mission_prep_backdrop_lag = 0.4f;
|
||||
// Visual recoil kick on each of Lara's shots (0 = off, 1 = default
|
||||
// strength, up to 2) and a controller vibration per shot.
|
||||
float weapon_recoil = 1.0f;
|
||||
@@ -208,6 +316,12 @@ namespace trlvr
|
||||
|
||||
bool ui_passthrough = true;
|
||||
bool menu_world_locked = true;
|
||||
// The in-game pause menu: true pins it
|
||||
// level in the room at the heading the head had when it opened.
|
||||
bool pause_world_locked = true;
|
||||
// Hide the gameplay HUD (health, ammo, prompts) outright instead of
|
||||
// letting it fade. World markers and the binocular view stay.
|
||||
bool hide_gameplay_hud = false;
|
||||
float ui_scale = 0.6f;
|
||||
// 1 follows the headset exactly; lower values leave part of the
|
||||
// current head angle in the gameplay HUD so edge elements move toward
|
||||
@@ -215,6 +329,18 @@ namespace trlvr
|
||||
// policies.
|
||||
float hud_follow = 0.85f;
|
||||
|
||||
// The settings overlay in the headset: a square panel placed in the
|
||||
// room in front of the head when it opens (and at a recentre or a
|
||||
// change of these), distance metres away, width degrees wide, turned
|
||||
// yaw degrees to the left and pitch degrees up. Holding the menu
|
||||
// button open_hold seconds opens it (0 = off: the button always goes
|
||||
// to the game).
|
||||
float overlay_vr_distance = 1.0f;
|
||||
float overlay_vr_width_degrees = 38.0f;
|
||||
float overlay_vr_yaw_degrees = 15.0f;
|
||||
float overlay_vr_pitch_degrees = -10.0f;
|
||||
float overlay_vr_open_hold = 0.6f;
|
||||
|
||||
// The safe production policy keeps portal/room culling bypassed and
|
||||
// widens only the engine's much cheaper object frustum.
|
||||
float cull_fov_degrees = 120.0f;
|
||||
@@ -297,6 +423,11 @@ namespace trlvr
|
||||
};
|
||||
|
||||
const Config& config();
|
||||
// For the in-game settings overlay: the live values, and one key
|
||||
// written to trlvr.ini. Everything else reads config().
|
||||
Config& config_edit();
|
||||
void config_save_setting(const wchar_t* section, const wchar_t* key,
|
||||
const wchar_t* value);
|
||||
// Hardcore first person from the pause menu: set and saved.
|
||||
void config_set_first_person_hardcore(bool on);
|
||||
// The other VR Settings toggles (pause menu, 2026-10-10): each sets the
|
||||
@@ -312,6 +443,7 @@ namespace trlvr
|
||||
// 0 classic, 1 shoulder, 2 board, 3 all (the view switch cycles them).
|
||||
void config_set_third_person_mode(int mode);
|
||||
void config_set_full_body(bool on);
|
||||
// 0 both, 1 stick, 2 sliders.
|
||||
// Hands against the world and Lara's body.
|
||||
void config_set_hand_collision(bool world, bool body);
|
||||
void load_config();
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
#include "log.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cstdarg>
|
||||
@@ -193,7 +194,7 @@ namespace trlvr
|
||||
|
||||
SYSTEMTIME st{};
|
||||
GetLocalTime(&st);
|
||||
fprintf(g_file, "TRL VR proxy log %04d-%02d-%02d %02d:%02d:%02d\n",
|
||||
fprintf(g_file, "TRL VR " TRLVR_VERSION " proxy log %04d-%02d-%02d %02d:%02d:%02d\n",
|
||||
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
||||
fflush(g_file);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
// The mod's version: the main menu watermark and the log's first line.
|
||||
#define TRLVR_VERSION "1.1.0"
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "../vr/hook.h"
|
||||
#include "../vr/tune.h"
|
||||
#include "../vr/vr_session.h"
|
||||
#include "backend.h"
|
||||
#include "proxy_d3d9.h"
|
||||
|
||||
@@ -38,6 +39,12 @@ extern "C"
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Before the backend, not after: DXVK creates its Vulkan instance
|
||||
// inside Direct3DCreate9, and by then it has to know which Vulkan
|
||||
// extensions the OpenXR runtime needs. vr_init leaves them where
|
||||
// the DXVK fork looks.
|
||||
vr_init();
|
||||
|
||||
IDirect3D9* d3d9 = real(SDKVersion);
|
||||
if (!d3d9)
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
// The settings overlay: a Dear ImGui window opened and closed with Insert,
|
||||
// or in the headset by holding the controller menu button (vr_input.cpp,
|
||||
// [vr] overlay_vr_open_hold). It edits the live values in config() and
|
||||
// saves each change to trlvr.ini, so the settings it covers no longer need
|
||||
// the game closed and the file edited.
|
||||
//
|
||||
// On the desktop mirror it is drawn onto the back buffer after the frame
|
||||
// has gone to the headset. With VR up it is also laid out in a square
|
||||
// texture (overlay_vr_texture) that vr_submit.cpp puts in the room as a
|
||||
// panel. Mouse, keyboard and controllers go to it, not the game, while it
|
||||
// is open.
|
||||
|
||||
#include <d3d9.h>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
// Once a frame, just before the real Present.
|
||||
void overlay_frame(IDirect3DSwapChain9* chain);
|
||||
// The overlay's device objects do not survive a Reset.
|
||||
void overlay_before_reset();
|
||||
// It holds a reference on the device; drop it before the device goes.
|
||||
void overlay_device_closing(IDirect3DDevice9* real_device);
|
||||
bool overlay_open();
|
||||
// Open or close it as Insert does. False if it is not up yet.
|
||||
bool overlay_request_open(bool open);
|
||||
// How many times it has opened (the panel is placed afresh each time).
|
||||
unsigned overlay_open_count();
|
||||
|
||||
// The controllers as a gamepad for the overlay: buttons as XInput's
|
||||
// XINPUT_GAMEPAD_* bits, sticks -1..1 (x right, y up). Given once a frame
|
||||
// while it is open (vr_input.cpp); a frame without one reads as nothing
|
||||
// held.
|
||||
struct OverlayPad
|
||||
{
|
||||
unsigned short buttons;
|
||||
float lx, ly, rx, ry;
|
||||
};
|
||||
void overlay_set_pad(const OverlayPad& pad);
|
||||
|
||||
// The headset pointer (vr_submit.cpp, 1.1.0): where the main hand's ray
|
||||
// meets the panel, u right and v down 0..1, and whether its trigger is
|
||||
// held. Given once a frame, before overlay_vr_texture, while it hits; a
|
||||
// frame without one reads as no pointer.
|
||||
struct OverlayPointer
|
||||
{
|
||||
float u, v;
|
||||
bool down;
|
||||
};
|
||||
void overlay_set_pointer(const OverlayPointer& pointer);
|
||||
|
||||
// While it is open: builds this frame's window laid out for the headset
|
||||
// and renders it into a square, premultiplied-alpha texture, which it
|
||||
// returns (the overlay keeps it). The desktop mirror then reuses the
|
||||
// same frame. Null when closed or if the texture cannot be made.
|
||||
IDirect3DTexture9* overlay_vr_texture();
|
||||
}
|
||||
@@ -107,9 +107,9 @@ namespace trlvr
|
||||
D3DPRESENT_PARAMETERS* pPresentationParameters,
|
||||
IDirect3DDevice9** ppReturnedDeviceInterface)
|
||||
{
|
||||
// Before the device, not after: the headset's eye shape decides what
|
||||
// size back buffer to ask for, and that has to be known now.
|
||||
vr_init();
|
||||
// The headset's eye shape decides what size back buffer to ask for,
|
||||
// and that has to be known before the device exists. The session was
|
||||
// brought up in Direct3DCreate9 (dllmain.cpp), ahead of the backend.
|
||||
|
||||
// Install the engine hooks once, before the game draws anything.
|
||||
static bool engine_init_done = false;
|
||||
@@ -165,8 +165,8 @@ namespace trlvr
|
||||
pPresentationParameters->FullScreen_RefreshRateInHz = 0;
|
||||
}
|
||||
|
||||
// SteamVR's recommended size already includes its global and
|
||||
// per-application resolution setting. Ask once before device creation;
|
||||
// The runtime's recommended size already includes its resolution
|
||||
// setting. Ask once before device creation;
|
||||
// changing it therefore takes effect the next time the game starts.
|
||||
bool sized_from_headset = false;
|
||||
if (vr_ready())
|
||||
@@ -175,7 +175,7 @@ namespace trlvr
|
||||
vr_render_target_size(&rw, &rh);
|
||||
if (rw && rh)
|
||||
{
|
||||
log(" back buffer %ux%u per eye from SteamVR's recommended render size",
|
||||
log(" back buffer %ux%u per eye from the runtime's recommended render size",
|
||||
rw, rh);
|
||||
pPresentationParameters->BackBufferWidth = rw & ~1u;
|
||||
pPresentationParameters->BackBufferHeight = rh & ~1u;
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace trlvr
|
||||
pPresentationParameters->Windowed = TRUE;
|
||||
pPresentationParameters->FullScreen_RefreshRateInHz = 0;
|
||||
}
|
||||
// Same sizing as CreateDevice (proxy_d3d9.cpp): SteamVR's
|
||||
// Same sizing as CreateDevice (proxy_d3d9.cpp): the runtime's
|
||||
// recommended per-eye size, then both eyes side by side. A reset
|
||||
// (a graphics option change) used to fall back to the window
|
||||
// size with one eye's width (2026-10-04).
|
||||
@@ -119,7 +119,7 @@ namespace trlvr
|
||||
pPresentationParameters->BackBufferHeight = rh & ~1u;
|
||||
sized_from_headset = true;
|
||||
log("device: reset back buffer %ux%u per eye from "
|
||||
"SteamVR's recommended render size", rw, rh);
|
||||
"the runtime's recommended render size", rw, rh);
|
||||
}
|
||||
}
|
||||
if (config().force_windowed && !sized_from_headset)
|
||||
@@ -158,6 +158,7 @@ namespace trlvr
|
||||
vr_submit_release_targets();
|
||||
reflection_stereo_release();
|
||||
imgui_overlay_release();
|
||||
overlay_before_reset();
|
||||
const HRESULT hr = _device->Reset(pPresentationParameters);
|
||||
gpu_profile_set_device(_device);
|
||||
|
||||
|
||||
+26
-13
@@ -17,6 +17,7 @@
|
||||
#include "../vr/reflection_stereo.h"
|
||||
#include "msaa_cap.h"
|
||||
#include "gpu_profile.h"
|
||||
#include "overlay.h"
|
||||
#include "../vr/perf_cpu.h"
|
||||
#include "../common/log.h"
|
||||
#include <cstring>
|
||||
@@ -41,6 +42,8 @@ namespace trlvr
|
||||
~ProxyDevice()
|
||||
{
|
||||
reflection_stereo_release();
|
||||
vr_submit_device_closing(_device);
|
||||
overlay_device_closing(_device);
|
||||
if (_device)
|
||||
_device->Release();
|
||||
// CreateDevice took a reference on the wrapper that made us.
|
||||
@@ -107,7 +110,7 @@ namespace trlvr
|
||||
STDMETHOD(GetBackBuffer)(THIS_ UINT iSwapChain, UINT iBackBuffer, D3DBACKBUFFER_TYPE Type, IDirect3DSurface9** ppBackBuffer) { return _device->GetBackBuffer(iSwapChain, iBackBuffer, Type, ppBackBuffer); }
|
||||
STDMETHOD(GetRasterStatus)(THIS_ UINT iSwapChain, D3DRASTER_STATUS* pRasterStatus) { return _device->GetRasterStatus(iSwapChain, pRasterStatus); }
|
||||
STDMETHOD(SetDialogBoxMode)(THIS_ BOOL bEnableDialogs) { return _device->SetDialogBoxMode(bEnableDialogs); }
|
||||
STDMETHOD_(void, SetGammaRamp)(THIS_ UINT iSwapChain, DWORD Flags, CONST D3DGAMMARAMP* pRamp) { _device->SetGammaRamp(iSwapChain, Flags, pRamp); }
|
||||
STDMETHOD_(void, SetGammaRamp)(THIS_ UINT iSwapChain, DWORD Flags, CONST D3DGAMMARAMP* pRamp) { vr_submit_set_game_ramp(pRamp); _device->SetGammaRamp(iSwapChain, Flags, pRamp); }
|
||||
STDMETHOD_(void, GetGammaRamp)(THIS_ UINT iSwapChain, D3DGAMMARAMP* pRamp) { _device->GetGammaRamp(iSwapChain, pRamp); }
|
||||
STDMETHOD(CreateTexture)(THIS_ UINT Width, UINT Height, UINT Levels, DWORD Usage, D3DFORMAT Format, D3DPOOL Pool, IDirect3DTexture9** ppTexture, HANDLE* pSharedHandle) { if (Usage & D3DUSAGE_RENDERTARGET) stereo_widen(&Width, Height); return _device->CreateTexture(Width, Height, Levels, Usage, Format, Pool, ppTexture, pSharedHandle); }
|
||||
STDMETHOD(CreateVolumeTexture)(THIS_ UINT Width, UINT Height, UINT Depth, UINT Levels, DWORD Usage, D3DFORMAT Format, D3DPOOL Pool, IDirect3DVolumeTexture9** ppVolumeTexture, HANDLE* pSharedHandle) { return _device->CreateVolumeTexture(Width, Height, Depth, Levels, Usage, Format, Pool, ppVolumeTexture, pSharedHandle); }
|
||||
@@ -262,8 +265,10 @@ namespace trlvr
|
||||
D3DRS_TWOSIDEDSTENCILMODE };
|
||||
DWORD saved_stencil[8]{};
|
||||
bool restore_stencil = false;
|
||||
DWORD saved_zfunc = 0;
|
||||
bool restore_zfunc = false;
|
||||
// Diorama: the controller hands are drawn in front of everything
|
||||
// (see below), by depth range, not by switching the depth test
|
||||
// off.
|
||||
bool hands_in_front = false;
|
||||
if (vr_dark_room_active() && !camera_first_person_active())
|
||||
{
|
||||
const int gpu_hand = camera_first_person_current_gpu_hand();
|
||||
@@ -301,14 +306,13 @@ namespace trlvr
|
||||
// before reaching the handle): the board world outside
|
||||
// the box still holds depth under the dark room, and a
|
||||
// hand behind it was neither drawn nor marked. There the
|
||||
// hands are drawn over everything.
|
||||
if (vr_diorama_active() &&
|
||||
SUCCEEDED(_device->GetRenderState(D3DRS_ZFUNC,
|
||||
&saved_zfunc)))
|
||||
{
|
||||
_device->SetRenderState(D3DRS_ZFUNC, D3DCMP_ALWAYS);
|
||||
restore_zfunc = true;
|
||||
}
|
||||
// hands are drawn over everything. That was done with the
|
||||
// depth test off, which also let a hand's far side and
|
||||
// its fingers show through its near side (user
|
||||
// 2026-10-14). They are squeezed into the front sliver of
|
||||
// the depth range instead: nearer than anything else,
|
||||
// and still sorted against themselves and each other.
|
||||
hands_in_front = vr_diorama_active();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,8 +322,6 @@ namespace trlvr
|
||||
for (int s = 0; s < 8; ++s)
|
||||
_device->SetRenderState(kStencilStates[s],
|
||||
saved_stencil[s]);
|
||||
if (restore_zfunc)
|
||||
_device->SetRenderState(D3DRS_ZFUNC, saved_zfunc);
|
||||
if (restore_clip_planes)
|
||||
_device->SetRenderState(D3DRS_CLIPPLANEENABLE,
|
||||
saved_clip_planes);
|
||||
@@ -378,6 +380,17 @@ namespace trlvr
|
||||
for (int p = 0; p < passes; ++p)
|
||||
{
|
||||
stereo_begin_pass(_device, p);
|
||||
if (hands_in_front)
|
||||
{
|
||||
// stereo_end_draw puts the game's viewport back.
|
||||
D3DVIEWPORT9 front{};
|
||||
if (SUCCEEDED(_device->GetViewport(&front)))
|
||||
{
|
||||
front.MinZ = 0.0f;
|
||||
front.MaxZ = 0.02f;
|
||||
_device->SetViewport(&front);
|
||||
}
|
||||
}
|
||||
// The right eye's water samples its own reflection.
|
||||
reflection_stereo_eye_textures(_device, p);
|
||||
sample_hand_projection(p == 0 ? EyeLeft : EyeRight);
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
#include "gpu_profile.h"
|
||||
#include "../vr/perf_cpu.h"
|
||||
#include "window.h"
|
||||
#include "overlay.h"
|
||||
#include "../vr/vr_session.h"
|
||||
#include "../vr/vr_submit.h"
|
||||
#include "../vr/tune.h"
|
||||
#include "../vr/vr_input.h"
|
||||
#include "../vr/input_labels.h"
|
||||
#include "../vr/camera_head.h"
|
||||
#include "../vr/menu_watermark.h"
|
||||
#include "../vr/tr7_menu_hook.h"
|
||||
|
||||
#include "../common/config.h"
|
||||
#include "../common/log.h"
|
||||
@@ -57,6 +60,11 @@ namespace trlvr
|
||||
input_labels_update();
|
||||
}
|
||||
|
||||
// The version / runtime line on the main menu (game font, queued
|
||||
// for the next frame's flush).
|
||||
menu_watermark_frame();
|
||||
// TR7-Menu-Hook actions asked for in the settings overlay.
|
||||
tr7_menu_hook_frame();
|
||||
gpu_profile_mark("mod: VR submit (resolve, overlays, hands)");
|
||||
if (vr_submit_ready())
|
||||
{
|
||||
@@ -76,6 +84,10 @@ namespace trlvr
|
||||
else
|
||||
gpu_profile_frame_end();
|
||||
|
||||
// After the frame has gone to the headset, so the settings overlay
|
||||
// is on the desktop window only.
|
||||
overlay_frame(_chain);
|
||||
|
||||
const HRESULT hr = _chain->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags);
|
||||
perf_cpu_present_end();
|
||||
return hr;
|
||||
|
||||
+73
-2
@@ -31,11 +31,53 @@ namespace trlvr
|
||||
PFN_SetCursorPos g_real_set_cursor_pos = nullptr;
|
||||
bool g_cursor_fix_installed = false;
|
||||
|
||||
// The settings overlay takes the mouse (window_block_game_mouse).
|
||||
// The game does not only get the mouse as messages: it also polls
|
||||
// GetCursorPos for the camera and puts the cursor back with
|
||||
// SetCursorPos, so while blocked its SetCursorPos does nothing and
|
||||
// its GetCursorPos reports the point it last recentred to -- no
|
||||
// movement, as far as it can tell.
|
||||
const uintptr_t kGameGetCursorPosIat = 0x00EFD270;
|
||||
using PFN_GetCursorPos = BOOL(WINAPI*)(LPPOINT);
|
||||
PFN_GetCursorPos g_real_get_cursor_pos = nullptr;
|
||||
bool g_mouse_blocked = false;
|
||||
POINT g_recentre_point = { 0, 0 };
|
||||
bool g_have_recentre_point = false;
|
||||
|
||||
BOOL WINAPI game_get_cursor_pos(LPPOINT point)
|
||||
{
|
||||
if (g_mouse_blocked && point)
|
||||
{
|
||||
if (g_have_recentre_point)
|
||||
{
|
||||
*point = g_recentre_point;
|
||||
return TRUE;
|
||||
}
|
||||
// Not recentred yet: hold whatever it first reads.
|
||||
static POINT held{};
|
||||
static bool have_held = false;
|
||||
if (!have_held)
|
||||
have_held = g_real_get_cursor_pos(&held) != FALSE;
|
||||
*point = held;
|
||||
return TRUE;
|
||||
}
|
||||
return g_real_get_cursor_pos(point);
|
||||
}
|
||||
|
||||
BOOL WINAPI game_set_cursor_pos(int x, int y)
|
||||
{
|
||||
HWND hwnd = g_hwnd;
|
||||
if (_ReturnAddress() == reinterpret_cast<void*>(kRecentreReturn) &&
|
||||
hwnd && IsWindow(hwnd))
|
||||
const bool recentre =
|
||||
_ReturnAddress() == reinterpret_cast<void*>(kRecentreReturn);
|
||||
if (recentre)
|
||||
{
|
||||
g_recentre_point.x = x;
|
||||
g_recentre_point.y = y;
|
||||
g_have_recentre_point = true;
|
||||
}
|
||||
if (g_mouse_blocked)
|
||||
return TRUE;
|
||||
if (recentre && hwnd && IsWindow(hwnd))
|
||||
{
|
||||
const POINT centre = { x, y };
|
||||
MONITORINFO mi{ sizeof(mi) };
|
||||
@@ -88,6 +130,25 @@ namespace trlvr
|
||||
VirtualProtect(slot, sizeof(void*), old, &old);
|
||||
log("window: game mouse recentre redirected to the window's "
|
||||
"own client centre (SetCursorPos import patched)");
|
||||
|
||||
void* real_get = reinterpret_cast<void*>(
|
||||
GetProcAddress(user32, "GetCursorPos"));
|
||||
void** get_slot =
|
||||
reinterpret_cast<void**>(kGameGetCursorPosIat);
|
||||
if (real_get && *get_slot == real_get &&
|
||||
VirtualProtect(get_slot, sizeof(void*), PAGE_READWRITE,
|
||||
&old))
|
||||
{
|
||||
g_real_get_cursor_pos =
|
||||
reinterpret_cast<PFN_GetCursorPos>(real_get);
|
||||
*get_slot = reinterpret_cast<void*>(&game_get_cursor_pos);
|
||||
VirtualProtect(get_slot, sizeof(void*), old, &old);
|
||||
}
|
||||
else
|
||||
{
|
||||
log("window: game GetCursorPos import not as expected; "
|
||||
"the settings overlay cannot hold the camera still");
|
||||
}
|
||||
}
|
||||
__except(EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
@@ -240,6 +301,16 @@ namespace trlvr
|
||||
"moved inside before a click", cursor.x, cursor.y);
|
||||
}
|
||||
|
||||
HWND window_handle()
|
||||
{
|
||||
return g_hwnd;
|
||||
}
|
||||
|
||||
void window_block_game_mouse(bool blocked)
|
||||
{
|
||||
g_mouse_blocked = blocked && g_real_get_cursor_pos;
|
||||
}
|
||||
|
||||
void window_tick()
|
||||
{
|
||||
HWND hwnd = g_hwnd;
|
||||
|
||||
@@ -28,6 +28,11 @@ namespace trlvr
|
||||
// the game has re-applied, and releases the cursor when the window is not
|
||||
// in the foreground so alt-tab actually gets you out.
|
||||
void window_tick();
|
||||
// The game window once window_configure has seen it, else null.
|
||||
HWND window_handle();
|
||||
// While set, the game sees a mouse that does not move and cannot move
|
||||
// the cursor itself (the settings overlay has it).
|
||||
void window_block_game_mouse(bool blocked);
|
||||
|
||||
// Before a synthesised click: if the cursor has left the (foreground)
|
||||
// game window, put it back inside, so the click cannot land on -- and
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Tests for src/vr/board_leash (included by mathtest.cpp).
|
||||
#include "../vr/godmode/board_leash.h"
|
||||
|
||||
static void test_board_leash()
|
||||
{
|
||||
using namespace trlvr;
|
||||
// Inside the radius x/y stay; outside the anchor is pulled to exactly
|
||||
// the radius, direction kept; height always follows.
|
||||
float a[3] = { 0.0f, 0.0f, 0.0f };
|
||||
const float l1[3] = { 30.0f, 40.0f, 7.0f }; // 50 away
|
||||
board_leash(a, l1, 80.0f);
|
||||
CHECK(near(a[0], 0.0f) && near(a[1], 0.0f) && near(a[2], 7.0f));
|
||||
board_leash(a, l1, 20.0f); // pulled 30 along (0.6, 0.8)
|
||||
CHECK(near(a[0], 18.0f) && near(a[1], 24.0f) && near(a[2], 7.0f));
|
||||
CHECK(near(std::sqrt((30.0f - a[0]) * (30.0f - a[0]) + (40.0f - a[1]) * (40.0f - a[1])), 20.0f));
|
||||
const float l2[3] = { 30.0f, 40.0f, -3.0f };
|
||||
board_leash(a, l2, 80.0f);
|
||||
CHECK(near(a[0], 18.0f) && near(a[1], 24.0f) && near(a[2], -3.0f));
|
||||
// Radius 0 puts the anchor on her; a height band pulls only past its edge.
|
||||
board_leash(a, l1, 0.0f);
|
||||
CHECK(near(a[0], 30.0f) && near(a[1], 40.0f));
|
||||
float b[3] = { 0.0f, 0.0f, 0.0f };
|
||||
const float l3[3] = { 0.0f, 0.0f, 10.0f };
|
||||
board_leash(b, l3, 5.0f, 4.0f);
|
||||
CHECK(near(b[2], 6.0f));
|
||||
board_leash(b, l3, 5.0f, 40.0f);
|
||||
CHECK(near(b[2], 6.0f));
|
||||
|
||||
// The board stick's dead zone: 0 inside 0.2, its edge 0, full tilt 1.
|
||||
CHECK(near(board_stick_axis(0.1f), 0.0f));
|
||||
CHECK(near(board_stick_axis(-0.19f), 0.0f));
|
||||
CHECK(near(board_stick_axis(0.2f), 0.0f));
|
||||
CHECK(near(board_stick_axis(0.6f), 0.5f));
|
||||
CHECK(near(board_stick_axis(-0.6f), -0.5f));
|
||||
CHECK(near(board_stick_axis(1.0f), 1.0f));
|
||||
CHECK(near(board_stick_axis(1.5f), 1.0f));
|
||||
CHECK(near(board_stick_axis(std::nanf("")), 0.0f));
|
||||
|
||||
// Zoom: stick up (in > 0) brings the view closer, exp(-in*speed*dt).
|
||||
{
|
||||
float zoom = 1.0f, scale = 10.0f;
|
||||
CHECK(board_stick_step(&zoom, &scale, 1.0f, 0.0f, 1.0f, 0.7f, 0.1f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f));
|
||||
CHECK(near(zoom, std::exp(-0.1f)));
|
||||
CHECK(near(scale, 10.0f)); // no scale input
|
||||
// Down zooms out by the same ratio: back to 1.
|
||||
board_stick_step(&zoom, &scale, -1.0f, 0.0f, 1.0f, 0.7f, 0.1f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f);
|
||||
CHECK(near(zoom, 1.0f));
|
||||
// Clamped at both ends.
|
||||
for (int i = 0; i < 100; ++i)
|
||||
board_stick_step(&zoom, &scale, 1.0f, 0.0f, 5.0f, 0.7f, 0.1f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f);
|
||||
CHECK(near(zoom, 0.3f));
|
||||
CHECK(!board_stick_step(&zoom, &scale, 1.0f, 0.0f, 5.0f, 0.7f, 0.1f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f));
|
||||
for (int i = 0; i < 100; ++i)
|
||||
board_stick_step(&zoom, &scale, -1.0f, 0.0f, 5.0f, 0.7f, 0.1f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f);
|
||||
CHECK(near(zoom, 3.5f));
|
||||
}
|
||||
// Scale: stick up makes Lara bigger (a smaller shrink factor); clamped
|
||||
// to 1.25 .. 25 (Lara 140 .. 7 cm).
|
||||
{
|
||||
float zoom = 1.0f, scale = 10.0f;
|
||||
board_stick_step(&zoom, &scale, 0.0f, 1.0f, 1.0f, 0.7f, 0.2f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f);
|
||||
CHECK(near(scale, 10.0f * std::exp(-0.14f)));
|
||||
CHECK(near(zoom, 1.0f));
|
||||
for (int i = 0; i < 200; ++i)
|
||||
board_stick_step(&zoom, &scale, 0.0f, 1.0f, 5.0f, 0.7f, 0.1f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f);
|
||||
CHECK(near(scale, 1.25f));
|
||||
for (int i = 0; i < 200; ++i)
|
||||
board_stick_step(&zoom, &scale, 0.0f, -1.0f, 5.0f, 0.7f, 0.1f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f);
|
||||
CHECK(near(scale, 25.0f));
|
||||
}
|
||||
// A hitch (dt 0.5 or more), no time, or a zero speed changes nothing.
|
||||
{
|
||||
float zoom = 1.0f, scale = 10.0f;
|
||||
CHECK(!board_stick_step(&zoom, &scale, 1.0f, 1.0f, 1.0f, 0.7f, 0.5f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f));
|
||||
CHECK(!board_stick_step(&zoom, &scale, 1.0f, 1.0f, 1.0f, 0.7f, 0.0f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f));
|
||||
CHECK(!board_stick_step(&zoom, &scale, 1.0f, 1.0f, 0.0f, 0.0f, 0.1f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f));
|
||||
CHECK(near(zoom, 1.0f) && near(scale, 10.0f));
|
||||
// No input on an axis leaves it as it was, even outside the range.
|
||||
float wide = 1.0f;
|
||||
board_stick_step(&zoom, &wide, 1.0f, 0.0f, 1.0f, 0.7f, 0.1f,
|
||||
0.3f, 3.5f, 1.25f, 25.0f);
|
||||
CHECK(near(wide, 1.0f));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
// Included by mathtest.cpp: src/vr/body_push.* (general push response), the
|
||||
// rotation-vector helpers in vr_math.
|
||||
#include "../vr/physics/body_push.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// A point through a row-vector matrix.
|
||||
void bp_apply(const Mat4& m, const float p[3], float out[3])
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
out[k] = p[0] * m.m[0][k] + p[1] * m.m[1][k] + p[2] * m.m[2][k] + m.m[3][k];
|
||||
}
|
||||
|
||||
// A point turned about a pivot by a segment's rotation.
|
||||
void bp_turn(const Mat4& rot, const float pivot[3], const float p[3], float out[3])
|
||||
{
|
||||
const float rel[3] = { p[0] - pivot[0], p[1] - pivot[1], p[2] - pivot[2] };
|
||||
for (int k = 0; k < 3; ++k)
|
||||
out[k] = pivot[k] + rel[0] * rot.m[0][k] + rel[1] * rot.m[1][k] + rel[2] * rot.m[2][k];
|
||||
}
|
||||
|
||||
const float kBpMetre = 280.0f;
|
||||
const float kBpUp[3] = { 0.0f, 0.0f, 1.0f };
|
||||
// Head 17, chest 15, waist 14 at 1.75 / 1.38 / 1.29 m; hips (bone 1) 1.0 m;
|
||||
// shoulders 53 / 78 at 1.45 m, hips 4 / 9 at 0.95 m (x = 0: the tests
|
||||
// measure along the vertical).
|
||||
const float kBpPivot[8][3] = { { 0, 0, 1.75f * 280 }, { 0, 0, 1.38f * 280 }, { 0, 0, 1.29f * 280 },
|
||||
{ 0, 0, 1.0f * 280 }, { 0, 0, 1.45f * 280 }, { 0, 0, 1.45f * 280 },
|
||||
{ 0, 0, 0.95f * 280 }, { 0, 0, 0.95f * 280 } };
|
||||
|
||||
// A hand pressing the segment from the +x side (outward normal +x), at
|
||||
// `height_m` above the pivot, `depth_m` into the body.
|
||||
BodyPushContact bp_contact(int segment, float height_m, float depth_m)
|
||||
{
|
||||
BodyPushContact c;
|
||||
c.segment = segment;
|
||||
c.point[0] = 0.0f;
|
||||
c.point[1] = 0.0f;
|
||||
c.point[2] = kBpPivot[segment][2] + height_m * kBpMetre;
|
||||
c.normal[0] = 1.0f;
|
||||
c.depth = depth_m * kBpMetre;
|
||||
return c;
|
||||
}
|
||||
|
||||
float bp_angle(const BodyPushState& s, int seg)
|
||||
{
|
||||
return std::sqrt(s.shown[seg][0] * s.shown[seg][0] + s.shown[seg][1] * s.shown[seg][1]);
|
||||
}
|
||||
|
||||
bool bp_identity(const Mat4& m, float eps = 1e-5f)
|
||||
{
|
||||
for (int r = 0; r < 4; ++r)
|
||||
for (int c = 0; c < 4; ++c)
|
||||
if (std::fabs(m.m[r][c] - (r == c ? 1.0f : 0.0f)) > eps)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void bp_run(BodyPushState& s, const BodyPushParams& p, const BodyPushContact* c, int n,
|
||||
float seconds, float dt = 1.0f / 60)
|
||||
{
|
||||
for (float t = 0.0f; t < seconds; t += dt)
|
||||
body_push_step(s, p, c, n, kBpPivot, kBpUp, kBpMetre, dt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void test_body_push()
|
||||
{
|
||||
using namespace trlvr;
|
||||
const float rad = 0.017453293f;
|
||||
|
||||
// rotation_from_vector / rotation_to_vector: right-hand rule like
|
||||
// rotation_between, and a round trip (also near a half turn).
|
||||
{
|
||||
const float rv[3] = { 0, 0, 1.5707963f };
|
||||
const Mat4 a = rotation_from_vector(rv);
|
||||
const float from[3] = { 1, 0, 0 }, to[3] = { 0, 1, 0 };
|
||||
const Mat4 b = rotation_between(from, to);
|
||||
bool same = true;
|
||||
for (int r = 0; r < 3; ++r)
|
||||
for (int c = 0; c < 3; ++c)
|
||||
same = same && std::fabs(a.m[r][c] - b.m[r][c]) < 1e-5f;
|
||||
CHECK(same);
|
||||
const float vectors[][3] = { { 0.1f, -0.2f, 0.3f }, { -1.0f, 0.5f, 0.2f },
|
||||
{ 0.0f, 0.0f, 0.0f }, { 2.0f, 1.0f, -1.0f },
|
||||
{ 0.0f, 3.0f, 0.0f } };
|
||||
for (const auto& v : vectors)
|
||||
{
|
||||
float back[3] = {};
|
||||
CHECK(rotation_to_vector(rotation_from_vector(v), back));
|
||||
for (int k = 0; k < 3; ++k)
|
||||
CHECK(near(back[k], v[k], 1e-3f) || std::fabs(back[k] - v[k]) < 1e-3f);
|
||||
}
|
||||
const float bad[3] = { NAN, 0, 0 };
|
||||
CHECK(bp_identity(rotation_from_vector(bad)));
|
||||
Mat4 nan_matrix = Mat4::identity();
|
||||
nan_matrix.m[1][1] = NAN;
|
||||
float out[3];
|
||||
CHECK(!rotation_to_vector(nan_matrix, out));
|
||||
}
|
||||
|
||||
// No contact: identity, stays at rest, not active.
|
||||
{
|
||||
BodyPushState s;
|
||||
BodyPushParams p;
|
||||
bp_run(s, p, nullptr, 0, 2.0f);
|
||||
CHECK(!body_push_active(s));
|
||||
for (int i = 0; i < kPushSegments; ++i)
|
||||
CHECK(bp_identity(body_push_rotation(s, i)));
|
||||
CHECK(bp_identity(body_push_rotation(s, 8))); // out of range
|
||||
CHECK(bp_identity(body_push_rotation(s, -1)));
|
||||
}
|
||||
|
||||
// A sustained press on the chest from +x tilts her away (top toward -x),
|
||||
// within the maximum, and the waist follows a little.
|
||||
{
|
||||
BodyPushState s;
|
||||
BodyPushParams p;
|
||||
const BodyPushContact c = bp_contact(kPushChest, 0.13f, 0.05f);
|
||||
bp_run(s, p, &c, 1, 1.5f);
|
||||
CHECK(body_push_active(s));
|
||||
const float max = p.max_degrees[kPushChest] * rad;
|
||||
const float a = bp_angle(s, kPushChest);
|
||||
CHECK(a <= max + 1e-6f);
|
||||
CHECK(a > 0.6f * max);
|
||||
float top[3];
|
||||
const float point[3] = { 0, 0, kBpPivot[kPushChest][2] + 0.3f * kBpMetre };
|
||||
bp_turn(body_push_rotation(s, kPushChest), kBpPivot[kPushChest], point, top);
|
||||
CHECK(top[0] < -0.01f * kBpMetre * 0.3f * a); // away from the hand
|
||||
CHECK(std::fabs(top[1]) < 1e-3f * kBpMetre); // not sideways
|
||||
// The pivot itself stays put.
|
||||
float stay[3];
|
||||
bp_turn(body_push_rotation(s, kPushChest), kBpPivot[kPushChest], kBpPivot[kPushChest], stay);
|
||||
CHECK(near(stay[2], kBpPivot[kPushChest][2], 1e-5f));
|
||||
// Chain: waist follows by 0.4 of the chest, the head not at all.
|
||||
CHECK(bp_angle(s, kPushWaist) > 0.0f);
|
||||
CHECK(bp_angle(s, kPushWaist) < a);
|
||||
CHECK(near(s.angle[kPushWaist][0], 0.4f * s.angle[kPushChest][0], 0.02f));
|
||||
CHECK(bp_identity(body_push_rotation(s, kPushHead)));
|
||||
|
||||
// Released: comes back to exactly rest without swinging past it.
|
||||
const float dir[2] = { s.angle[kPushChest][0] / a, s.angle[kPushChest][1] / a };
|
||||
float peak = 0.0f, worst = 0.0f;
|
||||
for (int i = 0; i < 180; ++i)
|
||||
{
|
||||
body_push_step(s, p, nullptr, 0, kBpPivot, kBpUp, kBpMetre, 1.0f / 60);
|
||||
for (int seg = 0; seg < 3; ++seg)
|
||||
{
|
||||
const float along = s.angle[seg][0] * dir[0] + s.angle[seg][1] * dir[1];
|
||||
if (seg == kPushChest)
|
||||
peak = std::fmax(peak, std::fabs(along));
|
||||
worst = std::fmin(worst, along);
|
||||
}
|
||||
}
|
||||
CHECK(worst > -0.1f * a);
|
||||
CHECK(!body_push_active(s));
|
||||
CHECK(bp_identity(body_push_rotation(s, kPushChest), 1e-7f));
|
||||
}
|
||||
|
||||
// A press from the other side (-x) tilts the other way; a push from +y
|
||||
// tilts toward -y.
|
||||
{
|
||||
BodyPushState s;
|
||||
BodyPushParams p;
|
||||
BodyPushContact c = bp_contact(kPushHead, 0.1f, 0.05f);
|
||||
c.normal[0] = -1.0f;
|
||||
bp_run(s, p, &c, 1, 1.0f);
|
||||
float top[3];
|
||||
const float point[3] = { 0, 0, kBpPivot[kPushHead][2] + 0.2f * kBpMetre };
|
||||
bp_turn(body_push_rotation(s, kPushHead), kBpPivot[kPushHead], point, top);
|
||||
CHECK(top[0] > 0.0f);
|
||||
BodyPushState s2;
|
||||
BodyPushContact c2 = bp_contact(kPushHead, 0.1f, 0.05f);
|
||||
c2.normal[0] = 0.0f;
|
||||
c2.normal[1] = 1.0f;
|
||||
bp_run(s2, p, &c2, 1, 1.0f);
|
||||
bp_turn(body_push_rotation(s2, kPushHead), kBpPivot[kPushHead], point, top);
|
||||
CHECK(top[1] < 0.0f && std::fabs(top[0]) < 1e-3f * kBpMetre);
|
||||
// A push straight down on the head (normal up) leans nothing.
|
||||
BodyPushState s3;
|
||||
BodyPushContact c3 = bp_contact(kPushHead, 0.1f, 0.05f);
|
||||
c3.normal[0] = 0.0f;
|
||||
c3.normal[2] = 1.0f;
|
||||
bp_run(s3, p, &c3, 1, 1.0f);
|
||||
CHECK(!body_push_active(s3));
|
||||
}
|
||||
|
||||
// A push at the pivot height does nothing; below it the lean reverses.
|
||||
{
|
||||
BodyPushState s;
|
||||
BodyPushParams p;
|
||||
const BodyPushContact at = bp_contact(kPushWaist, 0.0f, 0.05f);
|
||||
bp_run(s, p, &at, 1, 1.0f);
|
||||
CHECK(!body_push_active(s));
|
||||
CHECK(bp_identity(body_push_rotation(s, kPushWaist), 1e-7f));
|
||||
const BodyPushContact below = bp_contact(kPushWaist, -0.06f, 0.05f);
|
||||
bp_run(s, p, &below, 1, 1.0f);
|
||||
float top[3];
|
||||
const float point[3] = { 0, 0, kBpPivot[kPushWaist][2] + 0.3f * kBpMetre };
|
||||
bp_turn(body_push_rotation(s, kPushWaist), kBpPivot[kPushWaist], point, top);
|
||||
CHECK(top[0] > 0.0f);
|
||||
// A deeper press tilts more, up to the cap.
|
||||
BodyPushState shallow, deep;
|
||||
const BodyPushContact c1 = bp_contact(kPushChest, 0.13f, 0.01f);
|
||||
const BodyPushContact c2 = bp_contact(kPushChest, 0.13f, 0.03f);
|
||||
bp_run(shallow, p, &c1, 1, 1.5f);
|
||||
bp_run(deep, p, &c2, 1, 1.5f);
|
||||
CHECK(bp_angle(deep, kPushChest) > 2.0f * bp_angle(shallow, kPushChest));
|
||||
}
|
||||
|
||||
// The arrival kick: capped at 2.5 m/s, needs 0.2 m/s into the surface,
|
||||
// and never goes past the segment's maximum.
|
||||
{
|
||||
BodyPushParams p;
|
||||
auto peak_for = [&](float into_mps, bool arriving) {
|
||||
BodyPushState s;
|
||||
BodyPushContact c = bp_contact(kPushChest, 0.13f, 0.0f);
|
||||
c.velocity[0] = -into_mps * kBpMetre; // into the surface (normal +x)
|
||||
c.arriving = arriving;
|
||||
body_push_step(s, p, &c, 1, kBpPivot, kBpUp, kBpMetre, 1.0f / 60);
|
||||
float peak = bp_angle(s, kPushChest);
|
||||
for (int i = 0; i < 120; ++i)
|
||||
{
|
||||
body_push_step(s, p, nullptr, 0, kBpPivot, kBpUp, kBpMetre, 1.0f / 60);
|
||||
peak = std::fmax(peak, bp_angle(s, kPushChest));
|
||||
}
|
||||
return peak;
|
||||
};
|
||||
const float max = p.max_degrees[kPushChest] * rad;
|
||||
const float at_cap = peak_for(2.5f, true);
|
||||
const float huge = peak_for(500.0f, true);
|
||||
CHECK(at_cap > 0.3f * max);
|
||||
CHECK(at_cap <= max);
|
||||
CHECK(huge <= max);
|
||||
CHECK(std::fabs(huge - at_cap) < 0.02f * max);
|
||||
const float mid = peak_for(1.0f, true);
|
||||
CHECK(mid > 0.0f && mid < at_cap);
|
||||
CHECK(peak_for(0.1f, true) == 0.0f); // too slow
|
||||
CHECK(peak_for(-2.0f, true) == 0.0f); // moving away
|
||||
CHECK(peak_for(2.5f, false) == 0.0f); // not the arrival frame
|
||||
}
|
||||
|
||||
// Chains: a head push moves the chest by 0.3 and the waist by 0.4 of that.
|
||||
{
|
||||
BodyPushState s;
|
||||
BodyPushParams p;
|
||||
p.max_degrees[kPushChest] = 15.0f; // keep the caps out of the ratio
|
||||
p.max_degrees[kPushWaist] = 15.0f;
|
||||
const BodyPushContact c = bp_contact(kPushHead, 0.1f, 0.05f);
|
||||
bp_run(s, p, &c, 1, 2.0f);
|
||||
const float head = std::fabs(s.angle[kPushHead][0]);
|
||||
CHECK(head > 0.5f * p.max_degrees[kPushHead] * rad);
|
||||
CHECK(near(std::fabs(s.angle[kPushChest][0]), 0.3f * head, 0.02f));
|
||||
CHECK(near(std::fabs(s.angle[kPushWaist][0]), 0.12f * head, 0.02f));
|
||||
// Chest push: the head is untouched.
|
||||
BodyPushState s2;
|
||||
const BodyPushContact c2 = bp_contact(kPushChest, 0.13f, 0.05f);
|
||||
bp_run(s2, p, &c2, 1, 1.0f);
|
||||
CHECK(s2.angle[kPushHead][0] == 0.0f && s2.angle[kPushHead][1] == 0.0f);
|
||||
// Two hands add (and the total stays inside the cap).
|
||||
BodyPushState s3;
|
||||
const BodyPushContact two[2] = { bp_contact(kPushChest, 0.13f, 0.05f),
|
||||
bp_contact(kPushChest, 0.13f, 0.05f) };
|
||||
p.max_degrees[kPushChest] = 4.0f;
|
||||
bp_run(s3, p, two, 2, 1.5f);
|
||||
CHECK(bp_angle(s3, kPushChest) <= 4.0f * rad + 1e-6f);
|
||||
// A soft hit (weight 0.5) leans half as far.
|
||||
BodyPushState s4, s5;
|
||||
BodyPushContact light = bp_contact(kPushChest, 0.13f, 0.01f);
|
||||
BodyPushContact full = light;
|
||||
light.weight = 0.5f;
|
||||
bp_run(s4, p, &light, 1, 1.5f);
|
||||
bp_run(s5, p, &full, 1, 1.5f);
|
||||
CHECK(near(bp_angle(s4, kPushChest), 0.5f * bp_angle(s5, kPushChest), 0.02f));
|
||||
}
|
||||
|
||||
// The settings: strength 0 and a zero maximum do nothing; strength 2 leans more.
|
||||
{
|
||||
BodyPushParams p;
|
||||
const BodyPushContact c = bp_contact(kPushChest, 0.13f, 0.01f);
|
||||
BodyPushState a, b, d;
|
||||
p.strength = 0.0f;
|
||||
bp_run(a, p, &c, 1, 1.0f);
|
||||
CHECK(!body_push_active(a));
|
||||
p.strength = 1.0f;
|
||||
bp_run(b, p, &c, 1, 1.5f);
|
||||
p.strength = 2.0f;
|
||||
bp_run(d, p, &c, 1, 1.5f);
|
||||
CHECK(bp_angle(d, kPushChest) > 1.5f * bp_angle(b, kPushChest));
|
||||
BodyPushState e;
|
||||
p.max_degrees[kPushChest] = 0.0f;
|
||||
bp_run(e, p, &c, 1, 1.0f);
|
||||
CHECK(bp_identity(body_push_rotation(e, kPushChest)));
|
||||
}
|
||||
|
||||
// Frame-rate independence (roughly): 30 and 120 Hz settle to the same lean.
|
||||
{
|
||||
BodyPushParams p;
|
||||
const BodyPushContact c = bp_contact(kPushChest, 0.13f, 0.02f);
|
||||
BodyPushState a, b;
|
||||
bp_run(a, p, &c, 1, 2.0f, 1.0f / 30);
|
||||
bp_run(b, p, &c, 1, 2.0f, 1.0f / 120);
|
||||
CHECK(near(bp_angle(a, kPushChest), bp_angle(b, kPushChest), 0.01f));
|
||||
}
|
||||
|
||||
// Garbage in: nothing non-finite comes out, and it recovers.
|
||||
{
|
||||
BodyPushState s;
|
||||
BodyPushParams p;
|
||||
BodyPushContact bad = bp_contact(kPushChest, 0.13f, 0.05f);
|
||||
bad.point[1] = NAN;
|
||||
BodyPushContact bad2 = bp_contact(kPushChest, 0.13f, 0.05f);
|
||||
bad2.normal[0] = INFINITY;
|
||||
BodyPushContact bad3 = bp_contact(kPushChest, 0.13f, 0.0f); // arrival only
|
||||
bad3.velocity[2] = NAN;
|
||||
bad3.arriving = true;
|
||||
BodyPushContact bad4 = bp_contact(kPushChest, 0.13f, 0.05f);
|
||||
bad4.segment = 9;
|
||||
BodyPushContact bad5 = bp_contact(kPushChest, 0.13f, NAN);
|
||||
BodyPushContact zero = bp_contact(kPushChest, 0.13f, 0.05f);
|
||||
zero.normal[0] = 0.0f;
|
||||
const BodyPushContact all[6] = { bad, bad2, bad3, bad4, bad5, zero };
|
||||
bp_run(s, p, all, 6, 0.5f);
|
||||
CHECK(!body_push_active(s));
|
||||
float nan_pivot[8][3];
|
||||
std::memcpy(nan_pivot, kBpPivot, sizeof(nan_pivot));
|
||||
nan_pivot[1][2] = NAN;
|
||||
const BodyPushContact good = bp_contact(kPushChest, 0.13f, 0.05f);
|
||||
body_push_step(s, p, &good, 1, nan_pivot, kBpUp, kBpMetre, 1.0f / 60);
|
||||
body_push_step(s, p, &good, 1, kBpPivot, kBpUp, NAN, 1.0f / 60);
|
||||
body_push_step(s, p, &good, 1, kBpPivot, kBpUp, kBpMetre, NAN);
|
||||
body_push_step(s, p, &good, 1, kBpPivot, kBpUp, kBpMetre, -1.0f);
|
||||
body_push_step(s, p, &good, 1, kBpPivot, kBpUp, kBpMetre, 50.0f); // one big step
|
||||
bool finite = true;
|
||||
for (int i = 0; i < kPushSegments; ++i)
|
||||
{
|
||||
const Mat4 r = body_push_rotation(s, i);
|
||||
for (int a = 0; a < 4; ++a)
|
||||
for (int b = 0; b < 4; ++b)
|
||||
finite = finite && std::isfinite(r.m[a][b]);
|
||||
}
|
||||
CHECK(finite);
|
||||
// NaN in the state itself is dropped.
|
||||
s.angle[1][0] = NAN;
|
||||
body_push_step(s, p, nullptr, 0, kBpPivot, kBpUp, kBpMetre, 1.0f / 60);
|
||||
CHECK(!body_push_active(s));
|
||||
CHECK(bp_identity(body_push_rotation(s, kPushChest)));
|
||||
// A bad up resets; a good contact works afterwards.
|
||||
const float nan_up[3] = { NAN, 0, 1 };
|
||||
bp_run(s, p, &good, 1, 0.5f);
|
||||
CHECK(body_push_active(s));
|
||||
body_push_step(s, p, &good, 1, kBpPivot, nan_up, kBpMetre, 1.0f / 60);
|
||||
CHECK(!body_push_active(s));
|
||||
bp_run(s, p, &good, 1, 0.5f);
|
||||
CHECK(body_push_active(s));
|
||||
body_push_reset(s);
|
||||
CHECK(!body_push_active(s));
|
||||
// Null contacts with a count, null pivot.
|
||||
body_push_step(s, p, nullptr, 3, nullptr, kBpUp, kBpMetre, 1.0f / 60);
|
||||
CHECK(!body_push_active(s));
|
||||
}
|
||||
|
||||
// A tilted "up" still leans away from the hand.
|
||||
{
|
||||
BodyPushState s;
|
||||
BodyPushParams p;
|
||||
const float up[3] = { 0.0f, 0.6f, 0.8f };
|
||||
BodyPushContact c = bp_contact(kPushChest, 0.13f, 0.05f);
|
||||
c.point[0] = 0.0f;
|
||||
c.point[1] = 0.13f * kBpMetre * up[1];
|
||||
c.point[2] = kBpPivot[kPushChest][2] + 0.13f * kBpMetre * up[2];
|
||||
for (float t = 0.0f; t < 1.5f; t += 1.0f / 60)
|
||||
body_push_step(s, p, &c, 1, kBpPivot, up, kBpMetre, 1.0f / 60);
|
||||
float top[3];
|
||||
const float point[3] = { 0, kBpPivot[kPushChest][1] + 0.3f * kBpMetre * up[1],
|
||||
kBpPivot[kPushChest][2] + 0.3f * kBpMetre * up[2] };
|
||||
bp_turn(body_push_rotation(s, kPushChest), kBpPivot[kPushChest], point, top);
|
||||
CHECK(top[0] < 0.0f);
|
||||
// The axis is horizontal to her up: the top stays on her up's plane.
|
||||
const float rel[3] = { top[0] - kBpPivot[kPushChest][0], top[1] - kBpPivot[kPushChest][1],
|
||||
top[2] - kBpPivot[kPushChest][2] };
|
||||
const float len = std::sqrt(rel[0] * rel[0] + rel[1] * rel[1] + rel[2] * rel[2]);
|
||||
CHECK(near(len, 0.3f * kBpMetre, 1e-3f));
|
||||
}
|
||||
|
||||
// Which volume pushes which segment.
|
||||
{
|
||||
CHECK(body_push_segment_for_volume("head", -1, 2) == kPushHead);
|
||||
CHECK(body_push_segment_for_volume("neck", -1, 2) == kPushHead);
|
||||
CHECK(body_push_segment_for_volume("chest", -1, 2) == kPushChest);
|
||||
CHECK(body_push_segment_for_volume("upper chest", -1, 2) == kPushChest);
|
||||
CHECK(body_push_segment_for_volume("clavicle B", -1, 2) == kPushChest);
|
||||
CHECK(body_push_segment_for_volume("soft", 0, 2) == kPushChest);
|
||||
CHECK(body_push_segment_for_volume("soft", 1, 2) == kPushChest);
|
||||
CHECK(body_push_segment_for_volume("soft", 2, 2) == kPushNone);
|
||||
CHECK(body_push_segment_for_volume("abdomen", -1, 2) == kPushWaist);
|
||||
CHECK(body_push_segment_for_volume("pelvis", -1, 2) == kPushHips);
|
||||
CHECK(body_push_segment_for_volume("upper arm A", -1, 2) == kPushArmA);
|
||||
CHECK(body_push_segment_for_volume("forearm A", -1, 2) == kPushArmA);
|
||||
CHECK(body_push_segment_for_volume("hand A", -1, 2) == kPushArmA);
|
||||
CHECK(body_push_segment_for_volume("upper arm B", -1, 2) == kPushArmB);
|
||||
CHECK(body_push_segment_for_volume("forearm B", -1, 2) == kPushArmB);
|
||||
CHECK(body_push_segment_for_volume("hand B", -1, 2) == kPushArmB);
|
||||
CHECK(body_push_segment_for_volume("thigh A", -1, 2) == kPushLegA);
|
||||
CHECK(body_push_segment_for_volume("shin A", -1, 2) == kPushLegA);
|
||||
CHECK(body_push_segment_for_volume("thigh B", -1, 2) == kPushLegB);
|
||||
CHECK(body_push_segment_for_volume("shin B", -1, 2) == kPushLegB);
|
||||
CHECK(body_push_segment_for_volume("foot A", -1, 2) == kPushNone);
|
||||
CHECK(body_push_segment_for_volume("hand", -1, 2) == kPushNone);
|
||||
CHECK(body_push_segment_for_volume("hand C", -1, 2) == kPushNone);
|
||||
CHECK(body_push_segment_for_volume(nullptr, -1, 2) == kPushNone);
|
||||
}
|
||||
|
||||
// Arms (AUDIT 22.6): the arm end follows the push, within 8 degrees; a
|
||||
// hand push swings more than an upper-arm push of the same depth; the
|
||||
// chest gets 0.2 of the drive; both chains work; it comes back.
|
||||
{
|
||||
BodyPushParams p;
|
||||
p.max_degrees[kPushChest] = 15.0f; // keep the caps out of the ratio
|
||||
for (int arm = kPushArmA; arm <= kPushArmB; ++arm)
|
||||
{
|
||||
BodyPushState hand, upper;
|
||||
const BodyPushContact ch = bp_contact(arm, -0.6f, 0.05f);
|
||||
const BodyPushContact cu = bp_contact(arm, -0.15f, 0.05f);
|
||||
bp_run(hand, p, &ch, 1, 1.5f);
|
||||
bp_run(upper, p, &cu, 1, 1.5f);
|
||||
const float max = p.max_degrees[arm] * rad;
|
||||
CHECK(bp_angle(hand, arm) <= max + 1e-6f);
|
||||
CHECK(bp_angle(hand, arm) > 0.6f * max);
|
||||
CHECK(bp_angle(upper, arm) > 0.0f);
|
||||
CHECK(bp_angle(hand, arm) > 1.5f * bp_angle(upper, arm));
|
||||
// The hand (0.6 m below the shoulder) moves with the push (-x).
|
||||
float end[3];
|
||||
const float point[3] = { 0, 0, kBpPivot[arm][2] - 0.6f * kBpMetre };
|
||||
bp_turn(body_push_rotation(hand, arm), kBpPivot[arm], point, end);
|
||||
CHECK(end[0] < -0.01f * kBpMetre);
|
||||
CHECK(std::fabs(end[1]) < 1e-3f * kBpMetre);
|
||||
// The chest tilts away by 0.2 of the arm's drive, the other arm not.
|
||||
const float chest = std::fabs(hand.angle[kPushChest][0]);
|
||||
CHECK(chest > 0.0f);
|
||||
CHECK(near(chest, 0.2f * std::fabs(hand.angle[arm][0]), 0.03f));
|
||||
float top[3];
|
||||
const float up_point[3] = { 0, 0, kBpPivot[kPushChest][2] + 0.3f * kBpMetre };
|
||||
bp_turn(body_push_rotation(hand, kPushChest), kBpPivot[kPushChest], up_point, top);
|
||||
CHECK(top[0] < 0.0f);
|
||||
const int other = arm == kPushArmA ? kPushArmB : kPushArmA;
|
||||
CHECK(hand.angle[other][0] == 0.0f && hand.angle[other][1] == 0.0f);
|
||||
// The clavicle's share is a plain fraction of the arm's turn.
|
||||
const Mat4 full = body_push_rotation(hand, arm);
|
||||
const Mat4 part = body_push_rotation(hand, arm, 0.25f);
|
||||
float rf[3], rp[3];
|
||||
CHECK(rotation_to_vector(full, rf) && rotation_to_vector(part, rp));
|
||||
for (int k = 0; k < 3; ++k)
|
||||
CHECK(near(rp[k], 0.25f * rf[k], 1e-3f) || std::fabs(rp[k] - 0.25f * rf[k]) < 1e-5f);
|
||||
// Released: back to exactly rest.
|
||||
for (int i = 0; i < 240; ++i)
|
||||
body_push_step(hand, p, nullptr, 0, kBpPivot, kBpUp, kBpMetre, 1.0f / 60);
|
||||
CHECK(!body_push_active(hand));
|
||||
CHECK(bp_identity(body_push_rotation(hand, arm), 1e-7f));
|
||||
}
|
||||
// An arm's setting of 0 turns it off.
|
||||
BodyPushState off;
|
||||
p.max_degrees[kPushArmA] = 0.0f;
|
||||
const BodyPushContact c = bp_contact(kPushArmA, -0.6f, 0.05f);
|
||||
bp_run(off, p, &c, 1, 1.0f);
|
||||
CHECK(bp_identity(body_push_rotation(off, kPushArmA)));
|
||||
}
|
||||
|
||||
// Hips: a pelvis push tips the upper body away within 3 degrees and it
|
||||
// returns; a waist push moves the hips by 0.3 of the waist's lean.
|
||||
{
|
||||
BodyPushParams p;
|
||||
BodyPushState s;
|
||||
const BodyPushContact c = bp_contact(kPushHips, 0.0f, 0.05f);
|
||||
bp_run(s, p, &c, 1, 1.5f);
|
||||
const float max = p.max_degrees[kPushHips] * rad;
|
||||
CHECK(bp_angle(s, kPushHips) <= max + 1e-6f);
|
||||
CHECK(bp_angle(s, kPushHips) > 0.6f * max);
|
||||
float top[3];
|
||||
const float point[3] = { 0, 0, kBpPivot[kPushHips][2] + 0.5f * kBpMetre };
|
||||
bp_turn(body_push_rotation(s, kPushHips), kBpPivot[kPushHips], point, top);
|
||||
CHECK(top[0] < 0.0f && std::fabs(top[1]) < 1e-3f * kBpMetre);
|
||||
CHECK(bp_identity(body_push_rotation(s, kPushWaist))); // no chain upward
|
||||
for (int i = 0; i < 240; ++i)
|
||||
body_push_step(s, p, nullptr, 0, kBpPivot, kBpUp, kBpMetre, 1.0f / 60);
|
||||
CHECK(!body_push_active(s));
|
||||
|
||||
BodyPushState w;
|
||||
p.max_degrees[kPushWaist] = 15.0f;
|
||||
p.max_degrees[kPushHips] = 15.0f;
|
||||
const BodyPushContact cw = bp_contact(kPushWaist, 0.06f, 0.05f);
|
||||
bp_run(w, p, &cw, 1, 2.0f);
|
||||
CHECK(std::fabs(w.angle[kPushWaist][0]) > 0.0f);
|
||||
CHECK(near(std::fabs(w.angle[kPushHips][0]), 0.3f * std::fabs(w.angle[kPushWaist][0]), 0.02f));
|
||||
// A chest push reaches the hips through the waist: 0.4 x 0.3.
|
||||
BodyPushState ch;
|
||||
const BodyPushContact cc = bp_contact(kPushChest, 0.13f, 0.05f);
|
||||
bp_run(ch, p, &cc, 1, 2.0f);
|
||||
CHECK(near(std::fabs(ch.angle[kPushHips][0]), 0.12f * std::fabs(ch.angle[kPushChest][0]), 0.02f));
|
||||
}
|
||||
|
||||
// Legs: tiny, and the foot moves under 2 cm however hard it is pushed.
|
||||
{
|
||||
BodyPushParams p;
|
||||
p.strength = 3.0f;
|
||||
p.max_degrees[kPushLegA] = 8.0f; // the foot limit must still hold
|
||||
p.max_degrees[kPushLegB] = 8.0f;
|
||||
for (int leg = kPushLegA; leg <= kPushLegB; ++leg)
|
||||
{
|
||||
BodyPushState s;
|
||||
BodyPushContact c = bp_contact(leg, -0.3f, 0.05f);
|
||||
c.velocity[0] = -2.5f * kBpMetre;
|
||||
c.arriving = true;
|
||||
float worst = 0.0f;
|
||||
for (float t = 0.0f; t < 2.0f; t += 1.0f / 60)
|
||||
{
|
||||
body_push_step(s, p, &c, 1, kBpPivot, kBpUp, kBpMetre, 1.0f / 60);
|
||||
c.arriving = false;
|
||||
float foot[3];
|
||||
const float toe[3] = { 0, 0, kBpPivot[leg][2] - 0.9f * kBpMetre };
|
||||
bp_turn(body_push_rotation(s, leg), kBpPivot[leg], toe, foot);
|
||||
const float moved = std::sqrt((foot[0] - toe[0]) * (foot[0] - toe[0]) +
|
||||
(foot[1] - toe[1]) * (foot[1] - toe[1]) +
|
||||
(foot[2] - toe[2]) * (foot[2] - toe[2]));
|
||||
worst = std::fmax(worst, moved);
|
||||
}
|
||||
CHECK(worst <= 0.0201f * kBpMetre);
|
||||
CHECK(worst > 0.005f * kBpMetre); // it does move a little
|
||||
CHECK(bp_angle(s, leg) <= 2.3f * rad);
|
||||
// The foot goes with the push (-x), and it all comes back.
|
||||
float foot[3];
|
||||
const float toe[3] = { 0, 0, kBpPivot[leg][2] - 0.9f * kBpMetre };
|
||||
bp_turn(body_push_rotation(s, leg), kBpPivot[leg], toe, foot);
|
||||
CHECK(foot[0] < toe[0]);
|
||||
for (int i = 0; i < 240; ++i)
|
||||
body_push_step(s, p, nullptr, 0, kBpPivot, kBpUp, kBpMetre, 1.0f / 60);
|
||||
CHECK(!body_push_active(s));
|
||||
}
|
||||
// A short leg is cut harder; the other chain is untouched by a push.
|
||||
BodyPushState s;
|
||||
p.leg_reach_metres[0] = 0.45f;
|
||||
const BodyPushContact c = bp_contact(kPushLegA, -0.3f, 0.05f);
|
||||
bp_run(s, p, &c, 1, 1.5f);
|
||||
CHECK(bp_angle(s, kPushLegA) <= std::asin(0.02f / 0.45f) + 1e-6f);
|
||||
CHECK(s.angle[kPushLegB][0] == 0.0f && s.angle[kPushLegB][1] == 0.0f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
// Tests for src/vr/body_volumes (included by mathtest.cpp).
|
||||
#include "../vr/physics/body_volumes.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// A standing Lara: identity rotations, z up, facing -y (the root's
|
||||
// -y row), bone heights from docs/AUDIT.md 17.2 (cm at 100 per metre).
|
||||
struct BodyRig
|
||||
{
|
||||
float bones[153 * 16];
|
||||
float metre;
|
||||
int soft_bones[2];
|
||||
int soft_tips[2];
|
||||
|
||||
void place(int bone, float x_cm, float y_cm, float z_cm)
|
||||
{
|
||||
float* m = bones + bone * 16;
|
||||
for (int i = 0; i < 16; ++i)
|
||||
m[i] = (i % 5 == 0) ? 1.0f : 0.0f;
|
||||
m[12] = x_cm * metre / 100.0f;
|
||||
m[13] = y_cm * metre / 100.0f;
|
||||
m[14] = z_cm * metre / 100.0f;
|
||||
}
|
||||
|
||||
explicit BodyRig(float metre_in = 280.0f) : metre(metre_in)
|
||||
{
|
||||
for (int b = 0; b < 153; ++b)
|
||||
place(b, 0, 0, 0);
|
||||
place(1, -8, 0, 126);
|
||||
place(2, 8, 0, 126);
|
||||
place(3, 0, 0, 119);
|
||||
place(14, 0, 0, 132);
|
||||
place(15, 0, 0, 138);
|
||||
place(16, 0, 0, 170);
|
||||
place(17, 0, 0, 175);
|
||||
place(18, 0, 0, 196);
|
||||
place(4, -9, 0, 111); place(5, -9, 0, 64); place(6, -9, 0, 11);
|
||||
place(7, -9, 0, 0); place(8, -9, -10, 0);
|
||||
place(9, 9, 0, 111); place(10, 9, 0, 64); place(11, 9, 0, 11);
|
||||
place(12, 9, 0, 0); place(13, 9, -10, 0);
|
||||
place(52, -6, 0, 162); place(53, -27, 0, 161); place(54, -32, 0, 143);
|
||||
place(55, -36, 0, 126);
|
||||
for (int b = 56; b <= 71; ++b)
|
||||
place(b, -38, -4, 112);
|
||||
place(77, 6, 0, 162); place(78, 27, 0, 161); place(79, 32, 0, 143);
|
||||
place(80, 36, 0, 126);
|
||||
for (int b = 81; b <= 96; ++b)
|
||||
place(b, 38, -4, 112);
|
||||
// Soft parts: joint inside the chest, tip at the front.
|
||||
place(102, -8, -4, 150); place(103, -8, -12, 148);
|
||||
place(104, 8, -4, 150); place(105, 8, -12, 148);
|
||||
soft_bones[0] = 102; soft_bones[1] = 104;
|
||||
soft_tips[0] = 103; soft_tips[1] = 105;
|
||||
}
|
||||
|
||||
void build(trlvr::BodyVolumes& v) const
|
||||
{
|
||||
trlvr::body_volumes_build(v, bones, 153, metre, soft_bones, 2, soft_tips);
|
||||
}
|
||||
|
||||
// Turn about z by `angle` and move by (tx, ty, tz) units: row-vector
|
||||
// matrices, so every row (axes and position) is multiplied.
|
||||
void move(float angle, float tx, float ty, float tz)
|
||||
{
|
||||
const float c = std::cos(angle), s = std::sin(angle);
|
||||
for (int b = 0; b < 153; ++b)
|
||||
{
|
||||
float* m = bones + b * 16;
|
||||
for (int r = 0; r < 4; ++r)
|
||||
{
|
||||
const float x = m[r * 4], y = m[r * 4 + 1];
|
||||
m[r * 4] = x * c - y * s;
|
||||
m[r * 4 + 1] = x * s + y * c;
|
||||
}
|
||||
m[12] += tx; m[13] += ty; m[14] += tz;
|
||||
}
|
||||
}
|
||||
|
||||
void pt(float x_cm, float y_cm, float z_cm, float out[3]) const
|
||||
{
|
||||
out[0] = x_cm * metre / 100.0f;
|
||||
out[1] = y_cm * metre / 100.0f;
|
||||
out[2] = z_cm * metre / 100.0f;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static void test_body_volumes()
|
||||
{
|
||||
using namespace trlvr;
|
||||
BodyRig rig;
|
||||
BodyVolumes vols;
|
||||
rig.build(vols);
|
||||
CHECK(vols.valid);
|
||||
CHECK(vols.count > 15 && vols.count <= kBodyVolumesMax);
|
||||
|
||||
float p[3];
|
||||
BodyHit hit;
|
||||
|
||||
// The chest's centre is inside the chest (firm).
|
||||
rig.pt(0, 0, 154, p);
|
||||
CHECK(body_volumes_test(vols, p, &hit));
|
||||
CHECK(hit.inside && hit.volume >= 0 && hit.soft == -1);
|
||||
CHECK(hit.depth > 0.0f);
|
||||
CHECK(std::strcmp(vols.volume[hit.volume].name, "chest") == 0);
|
||||
|
||||
// A metre in front of it is outside; so is a point a metre above.
|
||||
rig.pt(0, -100, 154, p);
|
||||
CHECK(!body_volumes_test(vols, p, &hit));
|
||||
CHECK(!hit.inside && hit.depth < 0.0f);
|
||||
rig.pt(0, 0, 300, p);
|
||||
CHECK(!body_volumes_test(vols, p, nullptr));
|
||||
const float nan3[3] = { NAN, 0, 0 };
|
||||
CHECK(!body_volumes_test(vols, nan3, &hit));
|
||||
|
||||
// In front of the breast, at breast height: the soft part (index 1 on
|
||||
// her +x side, index 0 on -x), with the normal out of her front (-y).
|
||||
for (int side = 0; side < 2; ++side)
|
||||
{
|
||||
rig.pt(side ? 8.0f : -8.0f, -14, 148, p);
|
||||
CHECK(body_volumes_test(vols, p, &hit));
|
||||
CHECK(hit.soft == side);
|
||||
CHECK(hit.normal[1] < -0.9f);
|
||||
// The surface point is on the way out along the normal.
|
||||
float out[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
out[k] = p[k] + hit.normal[k] * hit.depth;
|
||||
CHECK(near(out[1], hit.surface[1], 1e-3f));
|
||||
// Pushed out by its depth plus a hair, it is no longer inside.
|
||||
BodyHit again;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
out[k] = p[k] + hit.normal[k] * (hit.depth + 0.01f * rig.metre);
|
||||
CHECK(!body_volumes_test(vols, out, &again) || again.soft != side);
|
||||
}
|
||||
CHECK(body_volumes_nearest_soft(vols, p, 0.08f * rig.metre) == 1);
|
||||
rig.pt(0, -14, 100, p);
|
||||
CHECK(body_volumes_nearest_soft(vols, p, 0.08f * rig.metre) == -1);
|
||||
|
||||
// The firm chest in front: its normal faces forward, deeper = more depth.
|
||||
float shallow[3], deep[3];
|
||||
rig.pt(0, -9, 160, shallow); // 2 cm under the chest's front face
|
||||
rig.pt(0, -3, 160, deep);
|
||||
BodyHit h1, h2;
|
||||
CHECK(body_volumes_test(vols, shallow, &h1) && body_volumes_test(vols, deep, &h2));
|
||||
CHECK(h1.soft == -1 && h1.normal[1] < -0.9f && h2.depth > h1.depth);
|
||||
|
||||
// Back of the chest points the other way.
|
||||
rig.pt(0, 8, 160, p);
|
||||
CHECK(body_volumes_test(vols, p, &hit) && hit.normal[1] > 0.9f);
|
||||
|
||||
// Limbs and head are capsules: a point on the forearm, the head.
|
||||
rig.pt(-34, 0, 134, p);
|
||||
CHECK(body_volumes_test(vols, p, &hit) && hit.volume >= 0);
|
||||
CHECK(std::strstr(vols.volume[hit.volume].name, "arm") != nullptr);
|
||||
CHECK(hit.normal[0] != 0.0f || hit.normal[1] != 0.0f || hit.normal[2] != 0.0f);
|
||||
rig.pt(0, -5, 186, p);
|
||||
CHECK(body_volumes_test(vols, p, &hit) &&
|
||||
std::strcmp(vols.volume[hit.volume].name, "head") == 0);
|
||||
CHECK(hit.normal[1] < -0.3f);
|
||||
rig.pt(-9, 0, 40, p);
|
||||
CHECK(body_volumes_test(vols, p, &hit) &&
|
||||
std::strstr(vols.volume[hit.volume].name, "shin A") != nullptr);
|
||||
|
||||
// Without the tip bones the soft volume sits in front of the joint.
|
||||
{
|
||||
BodyVolumes no_tips;
|
||||
body_volumes_build(no_tips, rig.bones, 153, rig.metre, rig.soft_bones, 2);
|
||||
rig.pt(-8, -12, 150, p);
|
||||
CHECK(no_tips.valid && body_volumes_test(no_tips, p, &hit) && hit.soft == 0);
|
||||
}
|
||||
|
||||
// Scaling: the same skeleton at another metre gives the same hits.
|
||||
{
|
||||
BodyRig big(250.0f);
|
||||
BodyVolumes v2;
|
||||
big.build(v2);
|
||||
big.pt(0, 0, 154, p);
|
||||
CHECK(body_volumes_test(v2, p, &hit) && hit.soft == -1);
|
||||
big.pt(-8, -14, 148, p);
|
||||
CHECK(body_volumes_test(v2, p, &hit) && hit.soft == 0);
|
||||
}
|
||||
|
||||
// The volumes move with the skeleton: turned a quarter circle about z
|
||||
// and moved, a hit in the new place and a miss in the old.
|
||||
{
|
||||
BodyRig moved;
|
||||
const float angle = 1.5707963f;
|
||||
moved.move(angle, 500.0f, -300.0f, 20.0f);
|
||||
BodyVolumes v2;
|
||||
moved.build(v2);
|
||||
CHECK(v2.valid);
|
||||
// Her old front (0, -y) is now along +x: forward = (cos, sin) of
|
||||
// (-y rotated by +90 degrees) = +x.
|
||||
CHECK(near(v2.forward[0], 1.0f, 1e-3f) && near(v2.forward[1], 0.0f, 1e-3f));
|
||||
float c[3] = { 0, 0, 154.0f * moved.metre / 100.0f };
|
||||
// centre of the chest: old (0,0,154) turned (no change) and moved.
|
||||
c[0] += 500.0f; c[1] += -300.0f; c[2] += 20.0f;
|
||||
CHECK(body_volumes_test(v2, c, &hit) && hit.soft == -1);
|
||||
CHECK(!body_volumes_test(vols, c, nullptr));
|
||||
// The left breast (-8, -14, 148) turned: (x,y) -> (x c - y s, x s + y c)
|
||||
// = (14, -8) cm, then moved.
|
||||
float q[3];
|
||||
moved.pt(14, -8, 148, q);
|
||||
q[0] += 500.0f; q[1] += -300.0f; q[2] += 20.0f;
|
||||
CHECK(body_volumes_test(v2, q, &hit) && hit.soft == 0);
|
||||
CHECK(hit.normal[0] > 0.9f); // out of her front, now +x
|
||||
}
|
||||
|
||||
// Bad input builds nothing and tests nothing.
|
||||
{
|
||||
BodyVolumes bad;
|
||||
body_volumes_build(bad, rig.bones, 10, rig.metre, nullptr, 0);
|
||||
CHECK(!bad.valid);
|
||||
body_volumes_build(bad, nullptr, 153, rig.metre, nullptr, 0);
|
||||
CHECK(!bad.valid);
|
||||
body_volumes_build(bad, rig.bones, 153, 0.0f, nullptr, 0);
|
||||
CHECK(!bad.valid);
|
||||
CHECK(!body_volumes_test(bad, p, &hit));
|
||||
BodyRig broken;
|
||||
broken.bones[16 * 3 + 12] = NAN;
|
||||
body_volumes_build(bad, broken.bones, 153, broken.metre, nullptr, 0);
|
||||
CHECK(!bad.valid);
|
||||
}
|
||||
// An added soft box (the buttock cheeks): hit by a point inside it,
|
||||
// refused on a bad set or bad axes.
|
||||
{
|
||||
BodyVolumes v2;
|
||||
rig.build(v2);
|
||||
const float* f = v2.forward;
|
||||
const float* u = v2.up;
|
||||
const float side[3] = { f[1] * u[2] - f[2] * u[1], f[2] * u[0] - f[0] * u[2], f[0] * u[1] - f[1] * u[0] };
|
||||
const float axes[3][3] = { { side[0], side[1], side[2] }, { f[0], f[1], f[2] }, { u[0], u[1], u[2] } };
|
||||
float c[3];
|
||||
rig.pt(0, 40, 60, c); // well behind her, low: in nothing else
|
||||
CHECK(!body_volumes_test(v2, c, &hit));
|
||||
const float half[3] = { 0.05f * rig.metre, 0.05f * rig.metre, 0.05f * rig.metre };
|
||||
const int before = v2.count;
|
||||
CHECK(body_volumes_add_box(v2, "rear test", c, axes, half, 5));
|
||||
CHECK(v2.count == before + 1);
|
||||
CHECK(body_volumes_test(v2, c, &hit) && hit.soft == 5);
|
||||
const float bent[3][3] = { { 2, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } };
|
||||
CHECK(!body_volumes_add_box(v2, "bad", c, bent, half, 6));
|
||||
BodyVolumes none;
|
||||
CHECK(!body_volumes_add_box(none, "bad", c, axes, half, 6));
|
||||
// An ellipsoid (the cheeks' touch zones): inside along its thin axis
|
||||
// the depth is the way to its surface, outside the gap; the one-volume
|
||||
// test agrees and ignores the others.
|
||||
float e[3];
|
||||
rig.pt(0, 40, 20, e);
|
||||
const float semi[3] = { 10.0f, 5.0f, 20.0f };
|
||||
const int at = v2.count;
|
||||
CHECK(body_volumes_add_box(v2, "rear ellipsoid", e, axes, semi, 7, BodyVolumeKind::Ellipsoid));
|
||||
float q[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
q[k] = e[k] + 4.0f * axes[1][k];
|
||||
CHECK(body_volumes_test_volume(v2, at, q, &hit) && hit.soft == 7);
|
||||
CHECK(near(hit.depth, 1.0f, 1e-3f) && near(hit.normal[0] * axes[1][0] + hit.normal[1] * axes[1][1] +
|
||||
hit.normal[2] * axes[1][2], 1.0f, 1e-3f));
|
||||
CHECK(body_volumes_test(v2, q, &hit) && hit.soft == 7);
|
||||
for (int k = 0; k < 3; ++k)
|
||||
q[k] = e[k] + 6.0f * axes[1][k];
|
||||
CHECK(!body_volumes_test_volume(v2, at, q, &hit) && near(hit.depth, -1.0f, 1e-3f));
|
||||
for (int k = 0; k < 3; ++k)
|
||||
q[k] = e[k] + 4.0f * axes[0][k] + 3.0f * axes[1][k]; // 0.16 + 0.36 < 1: inside, not a box corner
|
||||
CHECK(body_volumes_test_volume(v2, at, q, &hit));
|
||||
for (int k = 0; k < 3; ++k)
|
||||
q[k] = e[k] + 8.0f * axes[0][k] + 4.0f * axes[1][k]; // inside the box, outside the ellipsoid
|
||||
CHECK(!body_volumes_test_volume(v2, at, q, &hit));
|
||||
CHECK(!body_volumes_test_volume(v2, -1, q, &hit) && !body_volumes_test_volume(v2, 99, q, &hit));
|
||||
}
|
||||
}
|
||||
|
||||
// The Legend bone map: a standing skeleton with Legend's numbering (291 units
|
||||
// a metre), 109 bones.
|
||||
static void test_body_volumes_legend()
|
||||
{
|
||||
using namespace trlvr;
|
||||
const float metre = 291.0f;
|
||||
float bones[109 * 16];
|
||||
auto place = [&](int bone, float x_cm, float y_cm, float z_cm) {
|
||||
float* m = bones + bone * 16;
|
||||
for (int i = 0; i < 16; ++i)
|
||||
m[i] = (i % 5 == 0) ? 1.0f : 0.0f;
|
||||
m[12] = x_cm * metre / 100.0f;
|
||||
m[13] = y_cm * metre / 100.0f;
|
||||
m[14] = z_cm * metre / 100.0f;
|
||||
};
|
||||
for (int b = 0; b < 109; ++b)
|
||||
place(b, 0, 0, 0);
|
||||
place(1, 0, 0, 105); place(2, 0, 0, 125); place(3, 0, 0, 145);
|
||||
place(4, 0, 0, 165); place(5, 0, 0, 185);
|
||||
place(100, 0, 0, 100);
|
||||
place(54, -8, -4, 148); place(55, -8, -12, 146);
|
||||
place(56, 8, -4, 148); place(57, 8, -12, 146);
|
||||
place(58, -6, 0, 160); place(59, -26, 0, 158); place(60, -31, 0, 140); place(61, -35, 0, 122);
|
||||
place(79, 6, 0, 160); place(80, 26, 0, 158); place(81, 31, 0, 140); place(82, 35, 0, 122);
|
||||
place(101, -9, 0, 95); place(102, -9, 0, 52); place(103, -9, 0, 8); place(104, -9, -10, 0);
|
||||
place(105, 9, 0, 95); place(106, 9, 0, 52); place(107, 9, 0, 8); place(108, 9, -10, 0);
|
||||
|
||||
BodyVolumes vols;
|
||||
body_volumes_build(vols, bones, 109, metre, body_bone_map_legend());
|
||||
CHECK(vols.valid && vols.count > 15);
|
||||
auto find = [&](const char* name) {
|
||||
for (int i = 0; i < vols.count; ++i)
|
||||
if (std::strcmp(vols.volume[i].name, name) == 0)
|
||||
return i;
|
||||
return -1;
|
||||
};
|
||||
CHECK(find("head") >= 0 && find("chest") >= 0 && find("abdomen") >= 0);
|
||||
CHECK(find("upper arm A") >= 0 && find("forearm B") >= 0 && find("hand A") >= 0);
|
||||
CHECK(find("thigh A") >= 0 && find("foot B") >= 0);
|
||||
// Chain A is her left arm and leg (negative x here), B the other.
|
||||
const BodyVolume& ua = vols.volume[find("upper arm A")];
|
||||
const BodyVolume& tb = vols.volume[find("thigh B")];
|
||||
CHECK(ua.a[0] < 0.0f && tb.a[0] > 0.0f);
|
||||
// The hand carries on along the forearm (no finger bones).
|
||||
const BodyVolume& hand = vols.volume[find("hand A")];
|
||||
CHECK(hand.b[2] < hand.a[2]);
|
||||
// Both breasts are soft parts, with the part's index in BodyHit::soft.
|
||||
int soft = 0;
|
||||
for (int i = 0; i < vols.count; ++i)
|
||||
soft += vols.volume[i].soft >= 0;
|
||||
CHECK(soft == 2);
|
||||
BodyHit hit;
|
||||
const float front[3] = { 8.0f * metre / 100.0f, -12.0f * metre / 100.0f, 146.0f * metre / 100.0f };
|
||||
CHECK(body_volumes_test(vols, front, &hit) && hit.soft == 1);
|
||||
// Bone 100 is needed, and a skeleton too short for the map is refused.
|
||||
BodyVolumes bad;
|
||||
body_volumes_build(bad, bones, 100, metre, body_bone_map_legend());
|
||||
CHECK(!bad.valid);
|
||||
// The Underworld map is unchanged by the Legend one.
|
||||
CHECK(body_bone_map_underworld().head == 18 && body_bone_map_legend().head == 5);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
// Tests for src/vr/finger_input (included by mathtest.cpp).
|
||||
#include "../vr/finger_input.h"
|
||||
|
||||
static void test_finger_input()
|
||||
{
|
||||
using namespace trlvr;
|
||||
const float kOneMinusInvE = 1.0f - std::exp(-1.0f);
|
||||
|
||||
// Open hand: no sensors, nothing touched or pressed.
|
||||
{
|
||||
FingerInput open;
|
||||
float c[5], s[4];
|
||||
finger_curls_wanted(open, 0.0f, c, s);
|
||||
CHECK(near(c[0], 0.05f));
|
||||
CHECK(near(c[1], 0.0f));
|
||||
CHECK(near(c[2], 0.0f));
|
||||
CHECK(near(c[3], 0.0f));
|
||||
CHECK(near(c[4], 0.0f));
|
||||
CHECK(near(s[0], 0.55f));
|
||||
CHECK(near(s[1], 0.55f));
|
||||
CHECK(near(s[2], 0.55f));
|
||||
CHECK(near(s[3], 0.55f));
|
||||
}
|
||||
|
||||
// Full grip with sensors and the index finger pulled fully on the trigger.
|
||||
{
|
||||
FingerInput in;
|
||||
in.trigger = 1.0f;
|
||||
in.grip = 1.0f;
|
||||
in.has_sensors = true;
|
||||
in.touch_trigger = true;
|
||||
in.touch_thumb = true;
|
||||
float c[5], s[4];
|
||||
finger_curls_wanted(in, 0.0f, c, s);
|
||||
CHECK(near(c[0], 0.65f));
|
||||
CHECK(near(c[1], 0.9f));
|
||||
CHECK(near(c[2], 0.9f));
|
||||
CHECK(near(c[3], 0.9f));
|
||||
CHECK(near(c[4], 0.9f));
|
||||
CHECK(near(s[0], 0.4f));
|
||||
CHECK(near(s[3], 0.4f));
|
||||
}
|
||||
|
||||
// Sensors: a light reading without the touch flag is no finger on the
|
||||
// trigger; a real pull is (the sensor can drop out at full press).
|
||||
{
|
||||
FingerInput in;
|
||||
in.trigger = 0.15f;
|
||||
in.has_sensors = true;
|
||||
float c[5], s[4];
|
||||
finger_curls_wanted(in, 0.0f, c, s);
|
||||
CHECK(near(c[1], 0.0f));
|
||||
CHECK(near(c[0], 0.05f));
|
||||
in.trigger = 1.0f;
|
||||
finger_curls_wanted(in, 0.0f, c, s);
|
||||
CHECK(near(c[1], 0.9f));
|
||||
}
|
||||
|
||||
// A pull of 0.9 or more is a full one (worn triggers stop short of 1).
|
||||
{
|
||||
FingerInput in;
|
||||
in.has_sensors = true;
|
||||
in.touch_trigger = true;
|
||||
in.trigger = 0.92f;
|
||||
in.grip = 0.9f;
|
||||
float c[5], s[4];
|
||||
finger_curls_wanted(in, 0.0f, c, s);
|
||||
CHECK(near(c[1], 0.9f));
|
||||
CHECK(near(c[2], 0.9f));
|
||||
}
|
||||
|
||||
// No-sensor path: a pull above 0.05 counts as on the trigger, a face
|
||||
// button as the thumb.
|
||||
{
|
||||
FingerInput in;
|
||||
in.trigger = 0.5f;
|
||||
in.thumb_button = true;
|
||||
float c[5], s[4];
|
||||
finger_curls_wanted(in, 0.0f, c, s);
|
||||
CHECK(near(c[1], 0.35f + 0.55f * 0.5f / 0.9f));
|
||||
CHECK(near(c[0], 0.55f));
|
||||
|
||||
in.trigger = 0.04f;
|
||||
finger_curls_wanted(in, 0.0f, c, s);
|
||||
CHECK(near(c[1], 0.0f));
|
||||
}
|
||||
|
||||
// Without sensors the touch flags are ignored.
|
||||
{
|
||||
FingerInput in;
|
||||
in.touch_trigger = true;
|
||||
in.touch_thumb = true;
|
||||
float c[5], s[4];
|
||||
finger_curls_wanted(in, 0.0f, c, s);
|
||||
CHECK(near(c[1], 0.0f));
|
||||
CHECK(near(c[0], 0.05f));
|
||||
}
|
||||
|
||||
// The first step snaps to the wanted values and marks the state valid.
|
||||
{
|
||||
FingerCurls state;
|
||||
FingerInput in;
|
||||
in.grip = 1.0f;
|
||||
in.has_sensors = true;
|
||||
in.touch_thumb = true;
|
||||
finger_curls_step(state, in, 0.0f, 0.016f);
|
||||
CHECK(state.valid);
|
||||
CHECK(near(state.curl[0], 0.65f));
|
||||
CHECK(near(state.curl[2], 0.9f));
|
||||
CHECK(near(state.spread[0], 0.4f));
|
||||
}
|
||||
|
||||
// Easing: after one 50 ms time constant the index covers 1 - 1/e of the gap.
|
||||
{
|
||||
FingerCurls state;
|
||||
FingerInput open;
|
||||
finger_curls_step(state, open, 0.0f, 0.016f);
|
||||
FingerInput pull;
|
||||
pull.trigger = 1.0f;
|
||||
pull.has_sensors = true;
|
||||
pull.touch_trigger = true;
|
||||
finger_curls_step(state, pull, 0.0f, 0.05f);
|
||||
CHECK(near(state.curl[1], 0.9f * kOneMinusInvE));
|
||||
}
|
||||
|
||||
// A large dt (> 0.25 s) and a zero dt both snap.
|
||||
{
|
||||
FingerCurls state;
|
||||
FingerInput open;
|
||||
finger_curls_step(state, open, 0.0f, 0.016f);
|
||||
FingerInput pull;
|
||||
pull.trigger = 1.0f;
|
||||
pull.has_sensors = true;
|
||||
pull.touch_trigger = true;
|
||||
finger_curls_step(state, pull, 0.0f, 0.3f);
|
||||
CHECK(near(state.curl[1], 0.9f));
|
||||
|
||||
FingerCurls zero;
|
||||
finger_curls_step(zero, open, 0.0f, 0.016f);
|
||||
// dt 0 (a coarse clock that has not ticked) holds the pose: a
|
||||
// sensor flicker on such a frame must not snap a finger.
|
||||
finger_curls_step(zero, pull, 0.0f, 0.0f);
|
||||
CHECK(near(zero.curl[1], 0.0f));
|
||||
}
|
||||
|
||||
// Reset clears the state, so the next step snaps again.
|
||||
{
|
||||
FingerCurls state;
|
||||
FingerInput pull;
|
||||
pull.trigger = 1.0f;
|
||||
pull.has_sensors = true;
|
||||
pull.touch_trigger = true;
|
||||
finger_curls_step(state, pull, 0.0f, 0.016f);
|
||||
finger_curls_reset(state);
|
||||
CHECK(!state.valid);
|
||||
CHECK(near(state.curl[1], 0.0f));
|
||||
finger_curls_step(state, FingerInput(), 0.0f, 0.016f);
|
||||
CHECK(state.valid);
|
||||
CHECK(near(state.curl[1], 0.0f));
|
||||
}
|
||||
|
||||
// rest_curl floors index, middle, ring and pinky, not the thumb.
|
||||
{
|
||||
FingerInput open;
|
||||
float c[5], s[4];
|
||||
finger_curls_wanted(open, 0.3f, c, s);
|
||||
CHECK(near(c[0], 0.05f));
|
||||
CHECK(near(c[1], 0.3f));
|
||||
CHECK(near(c[2], 0.3f));
|
||||
CHECK(near(c[3], 0.3f));
|
||||
CHECK(near(c[4], 0.3f));
|
||||
|
||||
// Above the floor, the rest value changes nothing.
|
||||
FingerInput grip;
|
||||
grip.grip = 1.0f;
|
||||
grip.trigger = 1.0f;
|
||||
grip.has_sensors = true;
|
||||
grip.touch_trigger = true;
|
||||
finger_curls_wanted(grip, 0.3f, c, s);
|
||||
CHECK(near(c[1], 0.9f));
|
||||
CHECK(near(c[2], 0.9f));
|
||||
|
||||
// rest_curl is clamped to 0.6.
|
||||
finger_curls_wanted(open, 0.9f, c, s);
|
||||
CHECK(near(c[1], 0.6f));
|
||||
}
|
||||
// Gestures from what the controller does (finger_curls_wanted): grip
|
||||
// alone points (presses only), trigger + thumb pinches, trigger + grip is
|
||||
// a fist; both of those grab. A finger resting on the trigger is not a grab.
|
||||
{
|
||||
auto gesture = [](float trigger, float grip, bool on_trigger, bool thumb)
|
||||
{
|
||||
FingerInput in;
|
||||
in.has_sensors = true;
|
||||
in.trigger = trigger;
|
||||
in.grip = grip;
|
||||
in.touch_trigger = on_trigger;
|
||||
in.touch_thumb = thumb;
|
||||
float c[5], s[4];
|
||||
finger_curls_wanted(in, 0.0f, c, s);
|
||||
return finger_gesture(c);
|
||||
};
|
||||
CHECK(gesture(0.0f, 1.0f, false, true) == HandGesture::Point);
|
||||
CHECK(gesture(0.0f, 1.0f, false, false) == HandGesture::Point);
|
||||
CHECK(gesture(0.0f, 1.0f, true, true) != HandGesture::Fist); // resting, not pulling
|
||||
CHECK(!finger_gesture_grabs(gesture(0.0f, 1.0f, true, true)));
|
||||
CHECK(gesture(1.0f, 0.0f, true, true) == HandGesture::Pinch);
|
||||
CHECK(gesture(1.0f, 1.0f, true, true) == HandGesture::Fist);
|
||||
CHECK(gesture(1.0f, 1.0f, true, false) == HandGesture::Fist);
|
||||
CHECK(gesture(0.0f, 0.0f, false, false) == HandGesture::Open);
|
||||
CHECK(gesture(1.0f, 0.0f, true, false) == HandGesture::Open); // trigger alone, thumb up
|
||||
CHECK(finger_gesture_grabs(HandGesture::Pinch) && finger_gesture_grabs(HandGesture::Fist));
|
||||
CHECK(!finger_gesture_grabs(HandGesture::Point) && !finger_gesture_grabs(HandGesture::Open));
|
||||
}
|
||||
|
||||
// Grabbing with hysteresis: a fist starts it; a dip of the index to 0.5
|
||||
// (a flickering trigger) keeps it; an open index lets go; it does not
|
||||
// start again at 0.5.
|
||||
{
|
||||
const float fist[5] = { 0.6f, 0.9f, 0.9f, 0.9f, 0.9f };
|
||||
const float dip[5] = { 0.6f, 0.5f, 0.9f, 0.9f, 0.9f };
|
||||
const float open_index[5] = { 0.6f, 0.3f, 0.9f, 0.9f, 0.9f };
|
||||
const float pinch_dip[5] = { 0.35f, 0.5f, 0.0f, 0.0f, 0.0f };
|
||||
CHECK(finger_grabbing(fist, false));
|
||||
CHECK(finger_grabbing(dip, true));
|
||||
CHECK(!finger_grabbing(dip, false));
|
||||
CHECK(!finger_grabbing(open_index, true));
|
||||
CHECK(finger_grabbing(pinch_dip, true));
|
||||
CHECK(!finger_grabbing(pinch_dip, false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Included by mathtest.cpp: src/vr/god_carry.*
|
||||
#include "../vr/godmode/god_carry.h"
|
||||
|
||||
static void test_god_carry()
|
||||
{
|
||||
using namespace trlvr;
|
||||
// rotation_about_point: the pivot stays, a point at +x turns about z.
|
||||
{
|
||||
const float from[3] = { 1, 0, 0 }, to[3] = { 0, 1, 0 };
|
||||
const float pivot[3] = { 5, 5, 0 };
|
||||
const Mat4 r = rotation_about_point(rotation_between(from, to), pivot);
|
||||
const float p[3] = { 5, 5, 0 };
|
||||
float q[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
q[k] = p[0] * r.m[0][k] + p[1] * r.m[1][k] + p[2] * r.m[2][k] + r.m[3][k];
|
||||
CHECK(near(q[0], 5) && near(q[1], 5) && near(q[2], 0));
|
||||
const float a[3] = { 6, 5, 0 }; // 1 to the +x of the pivot -> 1 to +y
|
||||
for (int k = 0; k < 3; ++k)
|
||||
q[k] = a[0] * r.m[0][k] + a[1] * r.m[1][k] + a[2] * r.m[2][k] + r.m[3][k];
|
||||
CHECK(near(q[0], 5, 1e-3f) && near(q[1], 6, 1e-3f));
|
||||
}
|
||||
// bones_transform_range: only first..last (clipped) move.
|
||||
{
|
||||
Mat4 bones[6];
|
||||
for (Mat4& b : bones) b = Mat4::identity();
|
||||
bones_transform_range(bones, 6, 2, 99, translation(1, 2, 3));
|
||||
CHECK(bones[1].m[3][0] == 0.0f);
|
||||
CHECK(bones[2].m[3][0] == 1.0f && bones[5].m[3][2] == 3.0f);
|
||||
bones_transform_range(bones, 6, -4, 0, translation(7, 0, 0));
|
||||
CHECK(bones[0].m[3][0] == 7.0f && bones[1].m[3][0] == 0.0f);
|
||||
}
|
||||
// hang_arm_target: wrist straight above the shoulder, palm toward the head.
|
||||
{
|
||||
const float shoulder[3] = { 10, 20, 100 }, up[3] = { 0, 0, 1 };
|
||||
const float toward[3] = { 3, 0, 5 }; // head to +x (and above: ignored)
|
||||
Mat4 t;
|
||||
CHECK(hang_arm_target(shoulder, up, 60.0f, toward, true, true, &t));
|
||||
CHECK(near(t.m[3][0], 10) && near(t.m[3][1], 20) && near(t.m[3][2], 160));
|
||||
CHECK(near(t.m[0][2], 1.0f)); // chain A: fingers up
|
||||
// Left hand: palm direction = -z row = toward the head (+x).
|
||||
CHECK(near(-t.m[2][0], 1.0f) && near(t.m[2][2], 0.0f));
|
||||
// Rows orthonormal.
|
||||
for (int a = 0; a < 3; ++a)
|
||||
for (int b = 0; b < 3; ++b)
|
||||
{
|
||||
float d = 0;
|
||||
for (int k = 0; k < 3; ++k) d += t.m[a][k] * t.m[b][k];
|
||||
CHECK(near(d, a == b ? 1.0f : 0.0f, 1e-3f));
|
||||
}
|
||||
Mat4 u; // right arm, chain B: palm still toward the head (z row = +palm*r, r away)
|
||||
CHECK(hang_arm_target(shoulder, up, 60.0f, toward, false, false, &u));
|
||||
CHECK(near(u.m[0][2], -1.0f));
|
||||
CHECK(near(-u.m[2][0], 1.0f));
|
||||
Mat4 bad;
|
||||
const float para[3] = { 0, 0, 2 };
|
||||
CHECK(!hang_arm_target(shoulder, up, 60.0f, para, true, true, &bad));
|
||||
CHECK(!hang_arm_target(shoulder, up, 0.0f, toward, true, true, &bad));
|
||||
}
|
||||
// world_to_head_frame inverts "a hand at q in the head frame".
|
||||
{
|
||||
// An OpenXR head pose (3x4, column-vector): yaw 0.4 about y, pitch
|
||||
// -0.2 about x, then the translation in metres.
|
||||
float head[3][4] = {};
|
||||
{
|
||||
const float cy = std::cos(0.4f), sy = std::sin(0.4f);
|
||||
const float cp = std::cos(-0.2f), sp = std::sin(-0.2f);
|
||||
const float Ry[3][3] = { { cy, 0, sy }, { 0, 1, 0 }, { -sy, 0, cy } };
|
||||
const float Rx[3][3] = { { 1, 0, 0 }, { 0, cp, -sp }, { 0, sp, cp } };
|
||||
for (int i = 0; i < 3; ++i)
|
||||
for (int j = 0; j < 3; ++j)
|
||||
head[i][j] = Ry[i][0] * Rx[0][j] + Ry[i][1] * Rx[1][j] + Ry[i][2] * Rx[2][j];
|
||||
head[0][3] = 0.05f; head[1][3] = 0.3f; head[2][3] = -0.1f;
|
||||
}
|
||||
// The game's view-space transform of such a pose (z mirrored, the
|
||||
// rotation transposed for row vectors, translation in game units).
|
||||
auto game_from_xr_pose = [](const float p[3][4], float world_scale) {
|
||||
const float s[3] = { 1.0f, 1.0f, -1.0f };
|
||||
Mat4 m = Mat4::identity();
|
||||
for (int r = 0; r < 3; ++r)
|
||||
for (int c = 0; c < 3; ++c)
|
||||
m.m[c][r] = s[r] * p[r][c] * s[c];
|
||||
for (int c = 0; c < 3; ++c)
|
||||
m.m[3][c] = s[c] * p[c][3] * world_scale;
|
||||
return m;
|
||||
};
|
||||
const float ws = 291.0f * 12.0f;
|
||||
const Mat4 hv = game_from_xr_pose(head, ws);
|
||||
// Some camera: yawed, level (rows right, down, forward, position).
|
||||
Mat4 cam = Mat4::identity();
|
||||
const float fx = std::cos(0.7f), fy = std::sin(0.7f);
|
||||
cam.m[0][0] = fy; cam.m[0][1] = -fx; cam.m[0][2] = 0;
|
||||
cam.m[1][0] = 0; cam.m[1][1] = 0; cam.m[1][2] = -1;
|
||||
cam.m[2][0] = fx; cam.m[2][1] = fy; cam.m[2][2] = 0;
|
||||
cam.m[3][0] = 1000; cam.m[3][1] = -500; cam.m[3][2] = 300;
|
||||
const float q[3] = { 0.12f, -0.35f, -0.6f };
|
||||
// The hand's pose: the head's, moved by q in the head's own axes.
|
||||
float hand[3][4];
|
||||
for (int r = 0; r < 3; ++r)
|
||||
{
|
||||
for (int c = 0; c < 3; ++c) hand[r][c] = head[r][c];
|
||||
hand[r][3] = head[r][3] + head[r][0] * q[0] + head[r][1] * q[1] + head[r][2] * q[2];
|
||||
}
|
||||
const Mat4 gv = game_from_xr_pose(hand, ws);
|
||||
float v[3] = { gv.m[3][0], -gv.m[3][1], gv.m[3][2] };
|
||||
float w[3];
|
||||
for (int c = 0; c < 3; ++c)
|
||||
w[c] = v[0] * cam.m[0][c] + v[1] * cam.m[1][c] + v[2] * cam.m[2][c] + cam.m[3][c];
|
||||
float out[3];
|
||||
CHECK(world_to_head_frame(cam, hv, ws, w, out));
|
||||
CHECK(near(out[0], q[0], 1e-3f) && near(out[1], q[1], 1e-3f) && near(out[2], q[2], 1e-3f));
|
||||
Mat4 singular = Mat4::identity();
|
||||
for (int k = 0; k < 3; ++k) singular.m[k][k] = 0.0f;
|
||||
CHECK(!world_to_head_frame(singular, hv, ws, w, out));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
// Tests for src/vr/god_pluck (included by mathtest.cpp).
|
||||
#include "../vr/godmode/god_pluck.h"
|
||||
|
||||
static void test_god_pluck()
|
||||
{
|
||||
using namespace trlvr;
|
||||
PluckParams params;
|
||||
params.metre = 100.0f;
|
||||
params.table = 400.0f; // radius 30 + 16 = 46, z from -12 to 196
|
||||
|
||||
auto frame = [](float x, float y, float z, bool grip, int hand) {
|
||||
PluckInput in;
|
||||
in.dt = 1.0f / 60.0f;
|
||||
in.root[0] = 0.0f; in.root[1] = 0.0f; in.root[2] = 0.0f;
|
||||
for (int h = 0; h < 2; ++h)
|
||||
{
|
||||
in.point_valid[h] = true;
|
||||
in.point[h][0] = 1000.0f; // far away unless it is `hand`
|
||||
in.grip[h] = false;
|
||||
}
|
||||
in.point[hand][0] = x; in.point[hand][1] = y; in.point[hand][2] = z;
|
||||
in.grip[hand] = grip;
|
||||
return in;
|
||||
};
|
||||
|
||||
// Outside the zone: no pick-up, however it is gripped (sides, above, below).
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
const float out[4][3] = { { 50, 0, 50 }, { 0, 0, 200 }, { 0, 0, -20 },
|
||||
{ 0, 47, 10 } };
|
||||
for (const auto& o : out)
|
||||
{
|
||||
pluck_step(s, params, frame(o[0], o[1], o[2], false, 1), &ev);
|
||||
pluck_step(s, params, frame(o[0], o[1], o[2], true, 1), &ev);
|
||||
CHECK(!ev.picked_up);
|
||||
CHECK(s.hand == -1);
|
||||
pluck_step(s, params, frame(o[0], o[1], o[2], false, 1), &ev);
|
||||
}
|
||||
// The edges of the zone are inside.
|
||||
pluck_step(s, params, frame(46, 0, -12, false, 1), &ev);
|
||||
pluck_step(s, params, frame(46, 0, 196, true, 1), &ev);
|
||||
CHECK(ev.picked_up);
|
||||
}
|
||||
|
||||
// Head only: a grip at her middle does nothing; at her head (radius
|
||||
// 0.10 m x 100 + 0.01 x 400 = 14) it picks her up; no head, no pick-up.
|
||||
{
|
||||
PluckParams hp = params;
|
||||
hp.head_only = true;
|
||||
CHECK(near(pluck_head_radius(hp), 14.0f));
|
||||
auto at = [&](float x, float z, bool grip, bool head_valid) {
|
||||
PluckInput in = frame(x, 0, z, grip, 1);
|
||||
in.head_valid = head_valid;
|
||||
in.head[2] = 170.0f;
|
||||
return in;
|
||||
};
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, hp, at(0, 100, false, true), &ev);
|
||||
pluck_step(s, hp, at(0, 100, true, true), &ev);
|
||||
CHECK(!ev.picked_up && s.hand == -1);
|
||||
pluck_step(s, hp, at(0, 100, false, true), &ev);
|
||||
pluck_step(s, hp, at(10, 165, false, false), &ev);
|
||||
pluck_step(s, hp, at(10, 165, true, false), &ev);
|
||||
CHECK(!ev.picked_up && s.hand == -1);
|
||||
pluck_step(s, hp, at(10, 165, false, true), &ev);
|
||||
CHECK(s.inside[1]);
|
||||
pluck_step(s, hp, at(10, 165, true, true), &ev);
|
||||
CHECK(ev.picked_up && s.hand == 1);
|
||||
}
|
||||
|
||||
// Near her: a capsule from feet (0,0,0) to head top (0,0,180), reach
|
||||
// 0.6 m x 100 + 0.03 x 400 = 72.
|
||||
{
|
||||
const float feet[3] = { 0, 0, 0 }, top[3] = { 0, 0, 180 };
|
||||
const float in1[3] = { 70, 0, 90 }, out1[3] = { 74, 0, 90 };
|
||||
const float above[3] = { 0, 0, 250 }, far_above[3] = { 0, 0, 260 };
|
||||
CHECK(pluck_near(params, feet, top, in1, 0.6f));
|
||||
CHECK(!pluck_near(params, feet, top, out1, 0.6f));
|
||||
CHECK(pluck_near(params, feet, top, above, 0.6f));
|
||||
CHECK(!pluck_near(params, feet, top, far_above, 0.6f));
|
||||
PluckParams wide = params;
|
||||
wide.head_radius = 0.3f;
|
||||
CHECK(near(pluck_head_radius(wide), 34.0f));
|
||||
}
|
||||
|
||||
// A grip held before entering does not pick up; a hover event fires.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, params, frame(200, 0, 50, true, 1), &ev);
|
||||
pluck_step(s, params, frame(10, 0, 50, true, 1), &ev);
|
||||
CHECK(ev.hover_enter[1]);
|
||||
CHECK(!ev.picked_up && s.hand == -1);
|
||||
pluck_step(s, params, frame(10, 0, 50, true, 1), &ev);
|
||||
CHECK(!ev.picked_up && s.hand == -1);
|
||||
CHECK(s.hover);
|
||||
}
|
||||
|
||||
// Press inside picks up with that hand; grab offset = root - point.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, params, frame(10, -5, 50, false, 0), &ev);
|
||||
pluck_step(s, params, frame(10, -5, 50, true, 0), &ev);
|
||||
CHECK(ev.picked_up && ev.hand == 0);
|
||||
CHECK(s.hand == 0);
|
||||
CHECK(near(s.grab_offset[0], -10.0f) && near(s.grab_offset[1], 5.0f) &&
|
||||
near(s.grab_offset[2], -50.0f));
|
||||
|
||||
// The other hand cannot take her while held.
|
||||
PluckInput in = frame(10, -5, 50, true, 0);
|
||||
in.point[1][0] = 5; in.point[1][1] = 0; in.point[1][2] = 40;
|
||||
in.grip[1] = false;
|
||||
pluck_step(s, params, in, &ev);
|
||||
in.grip[1] = true;
|
||||
pluck_step(s, params, in, &ev);
|
||||
CHECK(!ev.picked_up && s.hand == 0);
|
||||
|
||||
// Still hand: tilts ~0 and target = point + grab offset.
|
||||
for (int i = 0; i < 120; ++i)
|
||||
pluck_step(s, params, frame(10, -5, 50, true, 0), &ev);
|
||||
CHECK(near(s.tilt[0], 0.0f, 1e-3f) && near(s.tilt[1], 0.0f, 1e-3f));
|
||||
CHECK(near(s.target[0], 0.0f) && near(s.target[1], 0.0f) &&
|
||||
near(s.target[2], 0.0f));
|
||||
CHECK(near(s.carry[0], 0.0f) && near(s.carry[2], 0.0f));
|
||||
}
|
||||
|
||||
// part_held blocks the pick-up.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, params, frame(10, 0, 50, false, 1), &ev);
|
||||
PluckInput in = frame(10, 0, 50, true, 1);
|
||||
in.part_held[1] = true;
|
||||
pluck_step(s, params, in, &ev);
|
||||
CHECK(!ev.picked_up && s.hand == -1);
|
||||
}
|
||||
|
||||
// Swing: accelerating +x swings her to -x first, then she settles; the
|
||||
// limbs lag; tilts stay within 1.2.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
float x = 10.0f;
|
||||
pluck_step(s, params, frame(x, 0, 50, false, 1), &ev);
|
||||
pluck_step(s, params, frame(x, 0, 50, true, 1), &ev);
|
||||
CHECK(s.hand == 1);
|
||||
for (int i = 0; i < 30; ++i)
|
||||
pluck_step(s, params, frame(x, 0, 50, true, 1), &ev);
|
||||
const float speed = 400.0f / 60.0f; // 1 table-m/s per frame
|
||||
float min_tilt = 0.0f, max_tilt = 0.0f;
|
||||
bool lag_ok = false;
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
x += speed;
|
||||
pluck_step(s, params, frame(x, 0, 50, true, 1), &ev);
|
||||
if (s.tilt[0] < min_tilt) min_tilt = s.tilt[0];
|
||||
if (s.tilt[0] > max_tilt) max_tilt = s.tilt[0];
|
||||
if (i == 2)
|
||||
lag_ok = fabsf(s.limb_tilt[2][0]) < fabsf(s.tilt[0]) &&
|
||||
fabsf(s.limb_tilt[0][0]) < fabsf(s.tilt[0]) &&
|
||||
fabsf(s.limb_tilt[2][0]) < fabsf(s.limb_tilt[0][0]);
|
||||
CHECK(fabsf(s.tilt[0]) <= 1.2f && fabsf(s.tilt[1]) <= 1.2f);
|
||||
}
|
||||
CHECK(min_tilt < -0.05f); // opposite to the acceleration
|
||||
CHECK(lag_ok);
|
||||
// The body direction leans to -x: straight down turned by the rotation.
|
||||
{
|
||||
const Mat4& r = s.body_rotation;
|
||||
float v[3];
|
||||
for (int j = 0; j < 3; ++j) v[j] = -r.m[2][j];
|
||||
CHECK(near(v[0], sinf(s.tilt[0]), 1e-3f));
|
||||
CHECK(near(v[2], -sqrtf(1.0f - v[0]*v[0] - v[1]*v[1]), 1e-3f));
|
||||
}
|
||||
// Stop: constant-speed motion decays, still hand settles.
|
||||
for (int i = 0; i < 60 * 5; ++i)
|
||||
pluck_step(s, params, frame(x, 0, 50, true, 1), &ev);
|
||||
CHECK(fabsf(s.tilt[0]) < 0.01f && fabsf(s.tilt[1]) < 0.01f);
|
||||
CHECK(fabsf(s.tilt_vel[0]) < 0.05f);
|
||||
}
|
||||
|
||||
// A violent jerk is clamped.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, params, frame(10, 0, 50, false, 1), &ev);
|
||||
pluck_step(s, params, frame(10, 0, 50, true, 1), &ev);
|
||||
float x = 10.0f;
|
||||
for (int i = 0; i < 200; ++i)
|
||||
{
|
||||
x += (i % 4 < 2) ? 80.0f : -80.0f;
|
||||
pluck_step(s, params, frame(x, 0, 50, true, 1), &ev);
|
||||
CHECK(fabsf(s.tilt[0]) <= 1.2f + 1e-6f);
|
||||
CHECK(fabsf(s.tilt[1]) <= 1.2f + 1e-6f);
|
||||
}
|
||||
}
|
||||
|
||||
// dt outside (0, 0.1]: no physics step.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, params, frame(10, 0, 50, false, 1), &ev);
|
||||
pluck_step(s, params, frame(10, 0, 50, true, 1), &ev);
|
||||
PluckInput in = frame(40, 0, 50, true, 1);
|
||||
in.dt = 0.0f;
|
||||
pluck_step(s, params, in, &ev);
|
||||
in.dt = 0.5f;
|
||||
pluck_step(s, params, in, &ev);
|
||||
CHECK(s.tilt[0] == 0.0f && s.tilt_vel[0] == 0.0f);
|
||||
CHECK(near(s.target[0], 30.0f)); // she still follows the hand
|
||||
}
|
||||
|
||||
// Release: with a floor -> dropped_to_floor; without -> not.
|
||||
for (int with_floor = 0; with_floor < 2; ++with_floor)
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, params, frame(10, 0, 50, false, 1), &ev);
|
||||
pluck_step(s, params, frame(10, 0, 50, true, 1), &ev);
|
||||
PluckInput in = frame(60, 20, 150, true, 1);
|
||||
in.floor_valid = with_floor != 0;
|
||||
in.floor[2] = 3.0f;
|
||||
pluck_step(s, params, in, &ev);
|
||||
CHECK(!ev.released && s.hand == 1);
|
||||
in.grip[1] = false;
|
||||
pluck_step(s, params, in, &ev);
|
||||
CHECK(ev.released && ev.hand == 1);
|
||||
CHECK(ev.dropped_to_floor == (with_floor != 0));
|
||||
CHECK(near(ev.target[0], 50.0f) && near(ev.target[1], 20.0f) &&
|
||||
near(ev.target[2], 100.0f));
|
||||
CHECK(s.hand == -1);
|
||||
CHECK(s.carry[0] == 0.0f && s.carry[1] == 0.0f && s.carry[2] == 0.0f);
|
||||
// She can be picked up again.
|
||||
pluck_step(s, params, frame(10, 0, 50, true, 1), &ev);
|
||||
CHECK(ev.picked_up);
|
||||
}
|
||||
|
||||
// pluck_reset while held clears; the old grip is not a new press.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, params, frame(10, 0, 50, false, 1), &ev);
|
||||
pluck_step(s, params, frame(30, 0, 50, true, 1), &ev);
|
||||
pluck_step(s, params, frame(60, 0, 50, true, 1), &ev);
|
||||
CHECK(s.hand == 1 && s.carry[0] != 0.0f);
|
||||
pluck_reset(s);
|
||||
CHECK(s.hand == -1);
|
||||
CHECK(s.carry[0] == 0.0f && s.carry[1] == 0.0f && s.carry[2] == 0.0f);
|
||||
pluck_step(s, params, frame(10, 0, 50, true, 1), &ev);
|
||||
CHECK(ev.picked_up); // fresh press after a reset
|
||||
// Disabled: dropped, and a held grip is not a press on return.
|
||||
params.enabled = false;
|
||||
pluck_step(s, params, frame(10, 0, 50, true, 1), &ev);
|
||||
CHECK(s.hand == -1);
|
||||
params.enabled = true;
|
||||
pluck_step(s, params, frame(10, 0, 50, true, 1), &ev);
|
||||
CHECK(!ev.picked_up);
|
||||
}
|
||||
|
||||
// Body matrix: the grab point lands on the hang point, with a tilt and
|
||||
// with a wrist pivot; rotation turns straight down into the body direction.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, params, frame(10, 0, 50, false, 1), &ev);
|
||||
pluck_step(s, params, frame(10, 0, 50, true, 1), &ev);
|
||||
float x = 10.0f;
|
||||
for (int i = 0; i < 15; ++i)
|
||||
{
|
||||
x += 6.0f;
|
||||
pluck_step(s, params, frame(x, 3, 50, true, 1), &ev);
|
||||
}
|
||||
CHECK(fabsf(s.tilt[0]) > 0.02f);
|
||||
PluckInput in = frame(x, 3, 50, true, 1);
|
||||
for (int pass = 0; pass < 2; ++pass)
|
||||
{
|
||||
in.wrist_pivot_valid = pass == 1;
|
||||
in.wrist_pivot[0] = 4.0f; in.wrist_pivot[1] = 2.0f;
|
||||
in.wrist_pivot[2] = 30.0f;
|
||||
const Mat4 m = pluck_body_matrix(s, in);
|
||||
float g[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
g[k] = pass ? in.wrist_pivot[k] : in.root[k] - s.grab_offset[k];
|
||||
float o[3];
|
||||
for (int j = 0; j < 3; ++j)
|
||||
o[j] = g[0]*m.m[0][j] + g[1]*m.m[1][j] + g[2]*m.m[2][j] +
|
||||
m.m[3][j];
|
||||
CHECK(near(o[0], s.hang[0], 1e-3f) && near(o[1], s.hang[1], 1e-3f) &&
|
||||
near(o[2], s.hang[2], 1e-3f));
|
||||
}
|
||||
// down * body_rotation == body direction
|
||||
const float tx = s.tilt[0], ty = s.tilt[1];
|
||||
const float bx = sinf(tx), by = sinf(ty);
|
||||
const float bz = -sqrtf(1.0f - bx*bx - by*by);
|
||||
const Mat4& r = s.body_rotation;
|
||||
CHECK(near(-r.m[2][0], bx, 1e-3f));
|
||||
CHECK(near(-r.m[2][1], by, 1e-3f));
|
||||
CHECK(near(-r.m[2][2], bz, 1e-3f));
|
||||
}
|
||||
|
||||
// Grace: a grip closing just after the hand left the zone (the pinch
|
||||
// point sinks into the palm) still picks her up; later, it does not.
|
||||
for (int late = 0; late < 2; ++late)
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, params, frame(10, 0, 50, false, 1), &ev);
|
||||
const int frames = late ? 18 : 6; // 0.3 s / 0.1 s out
|
||||
for (int i = 0; i < frames; ++i)
|
||||
pluck_step(s, params, frame(60, 0, 50, false, 1), &ev);
|
||||
pluck_step(s, params, frame(60, 0, 50, true, 1), &ev);
|
||||
CHECK(ev.picked_up == !late);
|
||||
}
|
||||
|
||||
// Settle: for 0.1 s after the grab the pinch moving (fingers closing)
|
||||
// does not move her; after it, she follows the hand again.
|
||||
{
|
||||
PluckParams sp = params;
|
||||
sp.settle_seconds = 0.1f;
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, sp, frame(10, 0, 50, false, 1), &ev);
|
||||
pluck_step(s, sp, frame(10, 0, 50, true, 1), &ev);
|
||||
CHECK(ev.picked_up);
|
||||
for (int i = 1; i <= 4; ++i)
|
||||
{
|
||||
pluck_step(s, sp, frame(10 - 5.0f * i, 0, 50, true, 1), &ev);
|
||||
CHECK(near(s.target[0], 0.0f) && near(s.carry[0], 0.0f));
|
||||
CHECK(s.hand_vel[0] == 0.0f);
|
||||
}
|
||||
for (int i = 0; i < 4; ++i)
|
||||
pluck_step(s, sp, frame(-10, 0, 50, true, 1), &ev);
|
||||
CHECK(s.settle <= 0.0f && near(s.target[0], 0.0f));
|
||||
pluck_step(s, sp, frame(0, 0, 50, true, 1), &ev);
|
||||
CHECK(near(s.target[0], 10.0f));
|
||||
}
|
||||
|
||||
// Segment distance.
|
||||
{
|
||||
const float a[3] = { 0, 0, 0 }, b[3] = { 10, 0, 0 };
|
||||
const float p1[3] = { 5, 3, 0 }, p2[3] = { -4, 0, 3 }, p3[3] = { 13, 4, 0 };
|
||||
CHECK(near(pluck_segment_distance(a, b, p1), 3.0f));
|
||||
CHECK(near(pluck_segment_distance(a, b, p2), 5.0f));
|
||||
CHECK(near(pluck_segment_distance(a, b, p3), 5.0f));
|
||||
}
|
||||
|
||||
// Arm zones: head ball at (0,0,170) radius 14; arm 0 from the shoulder
|
||||
// (20,0,140) to the fingertips (60,0,100), wrist (60,0,100), reach 60,
|
||||
// raised wrist (20,0,200); radius 0.05 x 100 + 0.01 x 400 = 9.
|
||||
{
|
||||
PluckParams ap = params;
|
||||
ap.head_only = true;
|
||||
ap.arm_radius = 0.05f;
|
||||
auto at = [&](float x, float y, float z, bool grip) {
|
||||
PluckInput in = frame(x, y, z, grip, 1);
|
||||
in.head_valid = true;
|
||||
in.head[2] = 170.0f;
|
||||
in.arm_valid[0] = true;
|
||||
const float sh[3] = { 20, 0, 140 }, wr[3] = { 60, 0, 100 }, pv[3] = { 20, 0, 200 };
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
in.arm_shoulder[0][k] = sh[k];
|
||||
in.arm_end[0][k] = wr[k];
|
||||
in.arm_wrist[0][k] = wr[k];
|
||||
in.arm_pivot[0][k] = pv[k];
|
||||
}
|
||||
in.arm_reach[0] = 60.0f;
|
||||
return in;
|
||||
};
|
||||
// Off the arm (10 from it): nothing; on it: arm 0, she is not lifted.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, ap, at(47, 0, 127, false), &ev);
|
||||
pluck_step(s, ap, at(47.1f, 0, 127.1f, true), &ev);
|
||||
CHECK(!ev.picked_up);
|
||||
}
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, ap, at(40, 0, 125, false), &ev);
|
||||
CHECK(s.inside[1]);
|
||||
pluck_step(s, ap, at(40, 0, 125, true), &ev);
|
||||
CHECK(ev.picked_up && ev.part == kPluckArm0 && s.part == kPluckArm0);
|
||||
CHECK(!s.lifted && !pluck_drawn(s));
|
||||
CHECK(near(s.arm_offset[0], 20.0f) && near(s.arm_offset[2], -25.0f));
|
||||
// The arm is nearly straight (56.6 of 60): 5 cm past that lifts.
|
||||
CHECK(near(s.lead_lift, sqrtf(3200.0f) + 5.0f, 0.01f));
|
||||
// A small pull: only the arm follows (wrist = pinch + offset).
|
||||
pluck_step(s, ap, at(20, 0, 150, true), &ev);
|
||||
CHECK(!s.lifted && !ev.lifted);
|
||||
CHECK(near(s.lead_target[0], 40.0f) && near(s.lead_target[2], 125.0f));
|
||||
CHECK(near(s.target[0], 0.0f) && near(s.target[2], 0.0f));
|
||||
// Let go before she leaves the ground: released, not moved.
|
||||
pluck_step(s, ap, at(20, 0, 150, false), &ev);
|
||||
CHECK(ev.released && !ev.moved && !ev.thrown && s.hand == -1);
|
||||
}
|
||||
// Pulled past the reach: lifted, starting along the arm's line.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, ap, at(40, 0, 125, false), &ev);
|
||||
pluck_step(s, ap, at(40, 0, 125, true), &ev);
|
||||
// Up and left over a second, to (-20,0,235): the wrist to (0,0,210).
|
||||
int lifts = 0;
|
||||
for (int i = 1; i <= 60; ++i)
|
||||
{
|
||||
const float f = i / 60.0f;
|
||||
pluck_step(s, ap, at(40 - 60 * f, 0, 125 + 110 * f, true), &ev);
|
||||
if (!ev.lifted)
|
||||
continue;
|
||||
++lifts;
|
||||
const float dx = s.hang[0] - 20.0f, dz = s.hang[2] - 140.0f;
|
||||
CHECK(sqrtf(dx * dx + dz * dz) > 60.0f);
|
||||
CHECK(near(s.tilt[0], asinf(-dx / sqrtf(dx * dx + dz * dz)), 0.05f));
|
||||
}
|
||||
CHECK(lifts == 1 && s.lifted && pluck_drawn(s));
|
||||
// Her feet hang below the raised wrist: hang + root - pivot.
|
||||
CHECK(near(s.hang[0], 0.0f) && near(s.hang[2], 210.0f));
|
||||
CHECK(near(s.target[0], -20.0f) && near(s.target[2], 10.0f));
|
||||
// The body matrix puts the raised wrist on the hang point.
|
||||
const PluckInput in = at(-20, 0, 235, true);
|
||||
const Mat4 m = pluck_body_matrix(s, in);
|
||||
const float* g = in.arm_pivot[0];
|
||||
float o[3];
|
||||
for (int j = 0; j < 3; ++j)
|
||||
o[j] = g[0]*m.m[0][j] + g[1]*m.m[1][j] + g[2]*m.m[2][j] + m.m[3][j];
|
||||
CHECK(near(o[0], 0.0f, 1e-3f) && near(o[2], 210.0f, 1e-3f));
|
||||
// She swings down under the hand.
|
||||
for (int i = 0; i < 60 * 5; ++i)
|
||||
pluck_step(s, ap, at(-20, 0, 235, true), &ev);
|
||||
CHECK(fabsf(s.tilt[0]) < 0.01f);
|
||||
pluck_step(s, ap, at(-20, 0, 235, false), &ev);
|
||||
CHECK(ev.released && ev.moved && s.hand == -1);
|
||||
}
|
||||
// No arm lead: lifted at the grab. Arm zones off: the arm is no zone.
|
||||
{
|
||||
PluckParams np = ap;
|
||||
np.arm_lead = false;
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, np, at(40, 0, 125, false), &ev);
|
||||
pluck_step(s, np, at(40, 0, 125, true), &ev);
|
||||
CHECK(ev.picked_up && s.lifted && pluck_drawn(s));
|
||||
np.arm_radius = 0.0f;
|
||||
PluckState s2;
|
||||
pluck_step(s2, np, at(40, 0, 125, false), &ev);
|
||||
pluck_step(s2, np, at(40, 0, 125, true), &ev);
|
||||
CHECK(!ev.picked_up);
|
||||
}
|
||||
// Near the head (12.8 of 14 from it, 23 from the arm): the head.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
pluck_step(s, ap, at(8, 0, 160, false), &ev);
|
||||
pluck_step(s, ap, at(8, 0, 160, true), &ev);
|
||||
CHECK(ev.picked_up && ev.part == kPluckHead && s.lifted);
|
||||
}
|
||||
}
|
||||
|
||||
// Throwing: a fast release flies, a slow one is put down.
|
||||
{
|
||||
PluckParams tp = params;
|
||||
tp.throw_enabled = true;
|
||||
const float dt = 1.0f / 60.0f;
|
||||
auto hold_and_move = [&](PluckState& s, float speed_mps, int frames, float* x) {
|
||||
PluckEvents ev;
|
||||
pluck_step(s, tp, frame(*x, 0, 50, false, 1), &ev);
|
||||
pluck_step(s, tp, frame(*x, 0, 50, true, 1), &ev);
|
||||
for (int i = 0; i < frames; ++i)
|
||||
{
|
||||
*x += speed_mps * tp.table * dt;
|
||||
pluck_step(s, tp, frame(*x, 0, 50, true, 1), &ev);
|
||||
}
|
||||
};
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
float x = 10.0f;
|
||||
hold_and_move(s, 0.2f, 30, &x);
|
||||
pluck_step(s, tp, frame(x, 0, 50, false, 1), &ev);
|
||||
CHECK(ev.released && !ev.thrown && !s.flying);
|
||||
}
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
float x = 10.0f;
|
||||
hold_and_move(s, 2.0f, 30, &x);
|
||||
// The hand stops for two frames as the fingers open: the peak counts.
|
||||
pluck_step(s, tp, frame(x, 0, 50, true, 1), &ev);
|
||||
pluck_step(s, tp, frame(x, 0, 50, true, 1), &ev);
|
||||
const float feet_x = s.target[0];
|
||||
pluck_step(s, tp, frame(x, 0, 50, false, 1), &ev);
|
||||
CHECK(ev.released && ev.thrown && ev.moved);
|
||||
CHECK(near(ev.throw_speed, 2.0f, 0.1f));
|
||||
CHECK(s.flying && s.hand == -1 && pluck_drawn(s));
|
||||
CHECK(near(s.fly_vel[0], ev.throw_speed * tp.table, 1.0f));
|
||||
// Flight: x grows at the throw speed, z falls under 9.81 m/s^2.
|
||||
const float vx = s.fly_vel[0], z0 = s.target[2];
|
||||
int frames = 0;
|
||||
for (; frames < 20; ++frames)
|
||||
{
|
||||
pluck_step(s, tp, frame(x, 0, 50, false, 1), &ev);
|
||||
CHECK(!ev.flight_over);
|
||||
}
|
||||
const float t = frames * dt;
|
||||
CHECK(near(s.target[0], feet_x + vx * t, 1.0f));
|
||||
CHECK(near(s.fly_vel[2], -9.81f * tp.table * t, 1.0f));
|
||||
CHECK(s.target[2] < z0 - 0.5f * 9.81f * tp.table * t * t * 0.9f);
|
||||
CHECK(near(s.fly_prev[0], s.target[0] - vx * dt, 1.0f));
|
||||
// A wall: moved back, sent back at a quarter of the speed; the
|
||||
// drawn offset (hang - feet) is kept. A ceiling turns the rise.
|
||||
{
|
||||
PluckState w = s;
|
||||
const float off = w.hang[2] - w.target[2];
|
||||
const float back[3] = { w.target[0] - 5.0f, 0.0f, w.target[2] };
|
||||
const float vz = w.fly_vel[2];
|
||||
pluck_bounce(w, back, 0.25f, false);
|
||||
CHECK(near(w.target[0], back[0]) && near(w.fly_prev[0], back[0]));
|
||||
CHECK(near(w.hang[2] - w.target[2], off));
|
||||
CHECK(near(w.fly_vel[0], -0.25f * vx, 1.0f) && near(w.fly_vel[2], vz));
|
||||
w.fly_vel[2] = 100.0f;
|
||||
pluck_bounce(w, back, 0.25f, true);
|
||||
CHECK(near(w.fly_vel[2], -25.0f));
|
||||
}
|
||||
// No catching her in flight: a press in the zone does nothing.
|
||||
pluck_step(s, tp, frame(10, 0, 50, true, 1), &ev);
|
||||
CHECK(!ev.picked_up && s.flying);
|
||||
// The flight ends after throw_max_time.
|
||||
bool over = false;
|
||||
for (int i = 0; i < 200 && !over; ++i)
|
||||
{
|
||||
pluck_step(s, tp, frame(10, 0, 50, true, 1), &ev);
|
||||
over = ev.flight_over;
|
||||
}
|
||||
CHECK(over && near(s.fly_time, tp.throw_max_time, 0.02f));
|
||||
pluck_land(s);
|
||||
CHECK(!s.flying && !pluck_drawn(s) && s.part == kPluckNone);
|
||||
// Picked up again with a fresh press.
|
||||
pluck_step(s, tp, frame(10, 0, 50, false, 1), &ev);
|
||||
pluck_step(s, tp, frame(10, 0, 50, true, 1), &ev);
|
||||
CHECK(ev.picked_up);
|
||||
}
|
||||
// A one-frame jump of the hand (tracking, a script ending) is no
|
||||
// throw; a very fast swing is capped at throw_max_speed.
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
float x = 10.0f;
|
||||
hold_and_move(s, 0.0f, 20, &x);
|
||||
pluck_step(s, tp, frame(x + 200.0f, 0, -100, true, 1), &ev); // ~40 m/s
|
||||
pluck_step(s, tp, frame(x + 200.0f, 0, -100, false, 1), &ev);
|
||||
CHECK(ev.released && !ev.thrown);
|
||||
}
|
||||
{
|
||||
PluckState s; PluckEvents ev;
|
||||
float x = 10.0f;
|
||||
hold_and_move(s, 10.0f, 20, &x);
|
||||
pluck_step(s, tp, frame(x, 0, 50, false, 1), &ev);
|
||||
CHECK(ev.thrown && near(ev.throw_speed, tp.throw_max_speed, 0.01f));
|
||||
CHECK(near(s.fly_vel[0], tp.throw_max_speed * tp.table, 2.0f));
|
||||
}
|
||||
// Throwing off: the same release is put down.
|
||||
{
|
||||
tp.throw_enabled = false;
|
||||
PluckState s; PluckEvents ev;
|
||||
float x = 10.0f;
|
||||
hold_and_move(s, 2.0f, 30, &x);
|
||||
pluck_step(s, tp, frame(x, 0, 50, false, 1), &ev);
|
||||
CHECK(ev.released && !ev.thrown && !s.flying);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
using namespace trlvr;
|
||||
|
||||
@@ -29,6 +30,25 @@ namespace
|
||||
return fabsf(a - b) <= eps;
|
||||
}
|
||||
|
||||
// For the god-mode test files (*_tests.inc): quiet, only failures print.
|
||||
int g_quiet_checks = 0;
|
||||
|
||||
void quiet_check(bool ok, const char* what, const char* file, int line)
|
||||
{
|
||||
++g_quiet_checks;
|
||||
if (!ok)
|
||||
{
|
||||
++g_failures;
|
||||
printf(" [FAIL] %s (%s:%d)\n", what, file, line);
|
||||
}
|
||||
}
|
||||
#define CHECK(cond) quiet_check((cond), #cond, __FILE__, __LINE__)
|
||||
|
||||
bool near(float a, float b, float eps = 1e-4f)
|
||||
{
|
||||
return fabsf(a - b) <= eps * (1.0f + fabsf(b));
|
||||
}
|
||||
|
||||
void print_regs(const char* label, const float* r)
|
||||
{
|
||||
printf(" %s\n", label);
|
||||
@@ -78,10 +98,35 @@ namespace
|
||||
};
|
||||
}
|
||||
|
||||
// God-mode modules ported from the Underworld mod: one file of tests each.
|
||||
#include "body_push_tests.inc"
|
||||
#include "body_volumes_tests.inc"
|
||||
#include "god_carry_tests.inc"
|
||||
#include "god_pluck_tests.inc"
|
||||
#include "soft_body_tests.inc"
|
||||
#include "rear_jiggle_tests.inc"
|
||||
#include "rear_shape_tests.inc"
|
||||
#include "board_leash_tests.inc"
|
||||
#include "menu_panel_tests.inc"
|
||||
#include "finger_input_tests.inc"
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("projection maths tests\n\n");
|
||||
|
||||
test_body_push();
|
||||
test_body_volumes();
|
||||
test_body_volumes_legend();
|
||||
test_god_carry();
|
||||
test_god_pluck();
|
||||
test_soft_body();
|
||||
test_rear_jiggle();
|
||||
test_rear_shape();
|
||||
test_board_leash();
|
||||
test_menu_panel();
|
||||
test_finger_input();
|
||||
printf(" %d god-mode checks run quietly\n\n", g_quiet_checks);
|
||||
|
||||
// --- reading the game's projection back out of its registers ----------
|
||||
GameProjection gp = projection_from_registers(kGameRegs);
|
||||
check(gp.valid, "recognised the game's registers as a projection");
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Tests for src/vr/menu_panel (included by mathtest.cpp).
|
||||
#include "../vr/menu_panel.h"
|
||||
|
||||
static void test_menu_panel()
|
||||
{
|
||||
using namespace trlvr;
|
||||
// Straight ahead (-z), no turn: 1 m out, width +x, height +y.
|
||||
const float head[3] = { 0.5f, 1.6f, -0.2f };
|
||||
MenuPanelPose p{};
|
||||
CHECK(menu_panel_place(head, 0.0f, -2.0f, 1.0f, 0.0f, 0.0f, &p));
|
||||
CHECK(near(p.centre[0], 0.5f) && near(p.centre[1], 1.6f) && near(p.centre[2], -1.2f));
|
||||
CHECK(near(p.right[0], 1.0f) && near(p.right[1], 0.0f) && near(p.right[2], 0.0f));
|
||||
CHECK(near(p.up[0], 0.0f) && near(p.up[1], 1.0f) && near(p.up[2], 0.0f));
|
||||
// 90 degrees left: out along -x, width now toward -z (still the
|
||||
// viewer's right as they face it).
|
||||
CHECK(menu_panel_place(head, 0.0f, -1.0f, 2.0f, 90.0f, 0.0f, &p));
|
||||
CHECK(near(p.centre[0], -1.5f) && near(p.centre[1], 1.6f) && near(p.centre[2], -0.2f));
|
||||
CHECK(near(p.right[0], 0.0f) && near(p.right[2], -1.0f));
|
||||
// Facing +x, whose right is +z: 90 degrees right (-90 left) goes there.
|
||||
CHECK(menu_panel_place(head, 1.0f, 0.0f, 1.0f, -90.0f, 0.0f, &p));
|
||||
CHECK(near(p.centre[0], 0.5f) && near(p.centre[2], 0.8f));
|
||||
// Pitched up 30: up 0.5 m at 1 m, top edge leaning back toward the head,
|
||||
// axes orthonormal and the panel square to the line of sight.
|
||||
CHECK(menu_panel_place(head, 0.0f, -1.0f, 1.0f, 0.0f, 30.0f, &p));
|
||||
CHECK(near(p.centre[1], 2.1f) && near(p.centre[2], -0.2f - 0.8660254f));
|
||||
CHECK(p.up[2] > 0.0f && near(p.up[1], 0.8660254f));
|
||||
{
|
||||
const float d[3] = { p.centre[0] - head[0], p.centre[1] - head[1], p.centre[2] - head[2] };
|
||||
const float du = d[0] * p.up[0] + d[1] * p.up[1] + d[2] * p.up[2];
|
||||
const float dr = d[0] * p.right[0] + d[1] * p.right[1] + d[2] * p.right[2];
|
||||
const float ur = p.up[0] * p.right[0] + p.up[1] * p.right[1] + p.up[2] * p.right[2];
|
||||
CHECK(near(du, 0.0f) && near(dr, 0.0f) && near(ur, 0.0f));
|
||||
}
|
||||
CHECK(!menu_panel_place(head, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, &p));
|
||||
// Corners: strip order TL, TR, BL, BR.
|
||||
CHECK(menu_panel_place(head, 0.0f, -1.0f, 1.0f, 0.0f, 0.0f, &p));
|
||||
float c[4][3];
|
||||
menu_panel_corners(p, 0.25f, c);
|
||||
CHECK(near(c[0][0], 0.25f) && near(c[0][1], 1.85f));
|
||||
CHECK(near(c[1][0], 0.75f) && near(c[1][1], 1.85f));
|
||||
CHECK(near(c[2][0], 0.25f) && near(c[2][1], 1.35f));
|
||||
CHECK(near(c[3][0], 0.75f) && near(c[3][1], 1.35f) && near(c[3][2], -1.2f));
|
||||
// 90 degrees wide at 1 m is 1 m each way.
|
||||
CHECK(near(menu_panel_half_size(1.0f, 90.0f), 1.0f));
|
||||
CHECK(near(menu_panel_half_size(2.0f, 38.0f), 2.0f * std::tan(19.0f * 0.01745329f)));
|
||||
|
||||
// Hold-to-open: a tap is a pause, a hold opens and never reaches the game.
|
||||
MenuHold h;
|
||||
bool game = true;
|
||||
CHECK(menu_hold_update(h, false, 1000, 0.6f, false, &game) == MenuHoldNone && !game);
|
||||
CHECK(menu_hold_update(h, true, 1000, 0.6f, false, &game) == MenuHoldNone && !game);
|
||||
CHECK(menu_hold_update(h, true, 1300, 0.6f, false, &game) == MenuHoldNone && !game);
|
||||
CHECK(menu_hold_update(h, false, 1400, 0.6f, false, &game) == MenuHoldTap && !game);
|
||||
CHECK(menu_hold_update(h, false, 1500, 0.6f, false, &game) == MenuHoldNone);
|
||||
CHECK(menu_hold_update(h, true, 2000, 0.6f, false, &game) == MenuHoldNone);
|
||||
CHECK(menu_hold_update(h, true, 2599, 0.6f, false, &game) == MenuHoldNone);
|
||||
CHECK(menu_hold_update(h, true, 2600, 0.6f, false, &game) == MenuHoldOpen && !game);
|
||||
// Now open: still down, then released, then pressed to close -- the
|
||||
// overlay's, never the game's, and no tap when it closes with it down.
|
||||
CHECK(menu_hold_update(h, true, 2700, 0.6f, true, &game) == MenuHoldNone && !game);
|
||||
CHECK(menu_hold_update(h, false, 2800, 0.6f, true, &game) == MenuHoldNone);
|
||||
CHECK(menu_hold_update(h, true, 3000, 0.6f, true, &game) == MenuHoldNone);
|
||||
CHECK(menu_hold_update(h, true, 3100, 0.6f, false, &game) == MenuHoldNone && !game);
|
||||
CHECK(menu_hold_update(h, true, 4000, 0.6f, false, &game) == MenuHoldNone && !game);
|
||||
CHECK(menu_hold_update(h, false, 4100, 0.6f, false, &game) == MenuHoldNone && !game);
|
||||
// Off: straight through, held as held.
|
||||
CHECK(menu_hold_update(h, true, 5000, 0.0f, false, &game) == MenuHoldNone && game);
|
||||
CHECK(menu_hold_update(h, true, 9000, 0.0f, false, &game) == MenuHoldNone && game);
|
||||
CHECK(menu_hold_update(h, false, 9100, 0.0f, false, &game) == MenuHoldNone && !game);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Tests for src/vr/rear_jiggle (included by mathtest.cpp).
|
||||
#include "../vr/physics/rear_jiggle.h"
|
||||
|
||||
static void test_rear_jiggle()
|
||||
{
|
||||
using namespace trlvr;
|
||||
JiggleParams p;
|
||||
p.hz = 3.0f;
|
||||
p.damping = 0.3f;
|
||||
p.max_offset = 5.0f;
|
||||
p.reset_distance = 50.0f;
|
||||
// At rest: no offset, ever.
|
||||
{
|
||||
JiggleState s;
|
||||
const float t[3] = { 1, 2, 3 };
|
||||
float off[3];
|
||||
bool still = true;
|
||||
for (int i = 0; i < 200; ++i)
|
||||
{
|
||||
jiggle_step(s, t, 1.0f / 90.0f, p, off);
|
||||
still = still && off[0] == 0.0f && off[1] == 0.0f && off[2] == 0.0f;
|
||||
}
|
||||
CHECK(still);
|
||||
}
|
||||
// Steady motion leaves no lag; a stop swings the mass on, then it settles.
|
||||
{
|
||||
JiggleState s;
|
||||
float t[3] = { 0, 0, 0 }, off[3] = {};
|
||||
const float dt = 1.0f / 90.0f;
|
||||
for (int i = 0; i < 270; ++i) // 3 s at 30 units/s
|
||||
{
|
||||
t[0] += 30.0f * dt;
|
||||
jiggle_step(s, t, dt, p, off);
|
||||
}
|
||||
CHECK(std::fabs(off[0]) < 0.01f);
|
||||
float peak = 0.0f;
|
||||
for (int i = 0; i < 90; ++i) // stopped
|
||||
{
|
||||
jiggle_step(s, t, dt, p, off);
|
||||
peak = off[0] > peak ? off[0] : peak;
|
||||
}
|
||||
CHECK(peak > 0.3f && peak <= 5.0f); // it carried on forward
|
||||
for (int i = 0; i < 450; ++i)
|
||||
jiggle_step(s, t, dt, p, off);
|
||||
CHECK(std::fabs(off[0]) < 0.01f); // settled
|
||||
}
|
||||
// The limit holds; a jump beyond reset_distance, or a bad dt, resets.
|
||||
{
|
||||
JiggleState s;
|
||||
float t[3] = { 0, 0, 0 }, off[3];
|
||||
jiggle_step(s, t, 0.01f, p, off);
|
||||
t[2] = 40.0f; // under the reset distance
|
||||
jiggle_step(s, t, 0.01f, p, off);
|
||||
CHECK(std::sqrt(off[0] * off[0] + off[1] * off[1] + off[2] * off[2]) <= 5.0f + 1e-4f);
|
||||
CHECK(off[2] < -1.0f); // left behind, downward
|
||||
t[2] = 200.0f;
|
||||
jiggle_step(s, t, 0.01f, p, off);
|
||||
CHECK(off[0] == 0.0f && off[1] == 0.0f && off[2] == 0.0f);
|
||||
t[2] = 210.0f;
|
||||
jiggle_step(s, t, 0.5f, p, off); // a hitch: reset, not a fling
|
||||
CHECK(off[2] == 0.0f);
|
||||
}
|
||||
// Contact: a press moves the mass with the hand; a grip holds it there
|
||||
// closely; on release it springs back; a kick sets it swinging.
|
||||
{
|
||||
JiggleParams q = p;
|
||||
q.touch_offset = 5.0f;
|
||||
JiggleState s;
|
||||
const float t[3] = { 0, 0, 0 };
|
||||
float off[3];
|
||||
const float dt = 1.0f / 90.0f;
|
||||
jiggle_step(s, t, dt, q, off);
|
||||
JiggleContact c;
|
||||
c.touched = true;
|
||||
c.shift[1] = -4.0f; // pressed 4 units in
|
||||
for (int i = 0; i < 90; ++i)
|
||||
jiggle_step(s, t, dt, q, off, &c);
|
||||
CHECK(off[1] < -3.0f && off[1] > -4.5f); // follows the press (limit loosened)
|
||||
c.gripped = true;
|
||||
c.shift[1] = 0.0f;
|
||||
c.shift[2] = 6.0f; // gripped and lifted
|
||||
float worst = 0.0f;
|
||||
for (int i = 0; i < 90; ++i)
|
||||
{
|
||||
jiggle_step(s, t, dt, q, off, &c);
|
||||
if (i > 45)
|
||||
worst = std::fabs(off[2] - 6.0f * std::tanh(6.0f / 10.0f) * 10.0f / 6.0f) > worst
|
||||
? std::fabs(off[2] - 6.0f * std::tanh(6.0f / 10.0f) * 10.0f / 6.0f) : worst;
|
||||
}
|
||||
CHECK(off[2] > 4.0f && worst < 0.2f); // held steadily (no ringing)
|
||||
float peak = 0.0f;
|
||||
for (int i = 0; i < 90; ++i) // released
|
||||
{
|
||||
jiggle_step(s, t, dt, q, off);
|
||||
peak = off[2] < peak ? off[2] : peak;
|
||||
}
|
||||
CHECK(peak < -0.5f); // swung past rest on the way back
|
||||
for (int i = 0; i < 360; ++i)
|
||||
jiggle_step(s, t, dt, q, off);
|
||||
CHECK(std::fabs(off[2]) < 0.05f && s.boost < 0.05f);
|
||||
JiggleContact k;
|
||||
k.kick[0] = 60.0f;
|
||||
jiggle_step(s, t, dt, q, off, &k);
|
||||
k.kick[0] = 0.0f;
|
||||
float swing = 0.0f;
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
jiggle_step(s, t, dt, q, off, &k);
|
||||
swing = off[0] > swing ? off[0] : swing;
|
||||
}
|
||||
CHECK(swing > 1.0f && swing < 5.0f + 5.0f);
|
||||
}
|
||||
// A hard stop far beyond the limit: the shown offset rounds off toward
|
||||
// the limit (no frame-to-frame jump near the peak), the mass is slowed
|
||||
// by the stiffening rather than parked on the safety wall.
|
||||
{
|
||||
JiggleParams q = p;
|
||||
q.max_offset = 2.0f;
|
||||
JiggleState s;
|
||||
float t[3] = { 0, 0, 0 }, off[3] = {};
|
||||
const float dt = 1.0f / 90.0f;
|
||||
for (int i = 0; i < 90; ++i)
|
||||
{
|
||||
t[0] += 300.0f * dt;
|
||||
jiggle_step(s, t, dt, q, off);
|
||||
}
|
||||
float peak = 0.0f, last = off[0], worst_step = 0.0f, mass_peak = 0.0f;
|
||||
for (int i = 0; i < 60; ++i)
|
||||
{
|
||||
if (i < 9) // stopping over 0.1 s
|
||||
t[0] += 300.0f * (1.0f - (float)(i + 1) / 9.0f) * dt;
|
||||
jiggle_step(s, t, dt, q, off);
|
||||
peak = off[0] > peak ? off[0] : peak;
|
||||
mass_peak = s.pos[0] - t[0] > mass_peak ? s.pos[0] - t[0] : mass_peak;
|
||||
if (off[0] > 1.5f && last > 1.5f && std::fabs(off[0] - last) > worst_step)
|
||||
worst_step = std::fabs(off[0] - last);
|
||||
last = off[0];
|
||||
}
|
||||
CHECK(peak > 1.5f && peak < 2.0f);
|
||||
CHECK(worst_step < 0.15f);
|
||||
CHECK(mass_peak < 3.0f * 2.0f - 0.1f);
|
||||
}
|
||||
// Frame rate: the same motion at 30 and 90 fps gives the same swing.
|
||||
{
|
||||
auto run = [&](int fps, float at, float out[3])
|
||||
{
|
||||
JiggleState s;
|
||||
const float dt = 1.0f / (float)fps;
|
||||
const int frames = (int)std::lround(at * fps);
|
||||
for (int i = 0; i <= frames; ++i)
|
||||
{
|
||||
const float time = i * dt;
|
||||
const float t[3] = { 0, 0, 8.0f * std::sin(6.0f * time) * (time < 1.0f ? 1.0f : 0.0f) +
|
||||
(time >= 1.0f ? 8.0f * std::sin(6.0f) : 0.0f) };
|
||||
jiggle_step(s, t, dt, p, out);
|
||||
}
|
||||
};
|
||||
float a[3], b[3];
|
||||
run(30, 1.2f, a);
|
||||
run(90, 1.2f, b);
|
||||
CHECK(std::fabs(a[2]) > 0.2f);
|
||||
CHECK(near(a[2], b[2], 0.05f * std::fabs(b[2]) + 0.02f));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// Tests for src/vr/rear_shape (included by mathtest.cpp).
|
||||
#include "../vr/physics/rear_shape.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
static void test_rear_shape()
|
||||
{
|
||||
using namespace trlvr::rear;
|
||||
const float metre = 291.0f;
|
||||
// The region weight: full at the centre and inside the core, none at
|
||||
// or past the surface, smooth and falling in between.
|
||||
{
|
||||
RearRegion r;
|
||||
r.centre[0] = 10.0f; r.centre[1] = 20.0f; r.centre[2] = 30.0f;
|
||||
r.radius[0] = 10.0f; r.radius[1] = 20.0f; r.radius[2] = 40.0f;
|
||||
r.core = 0.5f;
|
||||
const float c[3] = { 10.0f, 20.0f, 30.0f };
|
||||
CHECK(near(region_weight(r, c), 1.0f));
|
||||
const float in_core[3] = { 14.0f, 20.0f, 30.0f }; // d = 0.4
|
||||
CHECK(near(region_weight(r, in_core), 1.0f));
|
||||
const float surface[3] = { 10.0f, 40.0f, 30.0f }; // d = 1 along y
|
||||
CHECK(region_weight(r, surface) == 0.0f);
|
||||
const float out[3] = { 10.0f, 20.0f, 75.0f };
|
||||
CHECK(region_weight(r, out) == 0.0f);
|
||||
float last = 1.0f;
|
||||
bool falling = true, inside01 = true;
|
||||
for (int i = 0; i <= 20; ++i)
|
||||
{
|
||||
const float p[3] = { 10.0f + 0.5f * (float)i, 20.0f, 30.0f };
|
||||
const float w = region_weight(r, p);
|
||||
falling = falling && w <= last + 1e-6f;
|
||||
inside01 = inside01 && w >= 0.0f && w <= 1.0f;
|
||||
last = w;
|
||||
}
|
||||
CHECK(falling && inside01);
|
||||
// No core: only the centre is at full weight.
|
||||
r.core = 0.0f;
|
||||
CHECK(region_weight(r, in_core) < 0.99f && region_weight(r, in_core) > 0.5f);
|
||||
}
|
||||
// The seam: half on the line, each side keeps its own, none across.
|
||||
{
|
||||
CHECK(near(seam_weight(2.0f, 2.0f, 3.0f, 0), 0.5f));
|
||||
CHECK(near(seam_weight(2.0f, 2.0f, 3.0f, 1), 0.5f));
|
||||
CHECK(near(seam_weight(-1.0f, 2.0f, 3.0f, 0), 1.0f));
|
||||
CHECK(near(seam_weight(5.0f, 2.0f, 3.0f, 0), 0.0f, 1e-6f));
|
||||
CHECK(near(seam_weight(5.0f, 2.0f, 3.0f, 1), 1.0f));
|
||||
CHECK(near(seam_weight(-1.0f, 2.0f, 3.0f, 1), 0.0f, 1e-6f));
|
||||
CHECK(seam_weight(100.0f, 2.0f, 0.0f, 0) == 1.0f); // no cut
|
||||
// The two sides add to 1 everywhere (no gap, no double at the seam).
|
||||
bool sum1 = true;
|
||||
for (int i = -10; i <= 10; ++i)
|
||||
{
|
||||
const float x = 2.0f + 0.4f * (float)i;
|
||||
sum1 = sum1 && near(seam_weight(x, 2.0f, 3.0f, 0) + seam_weight(x, 2.0f, 3.0f, 1), 1.0f);
|
||||
}
|
||||
CHECK(sum1);
|
||||
}
|
||||
// The dent: a point inside goes out to the surface, along the line from
|
||||
// the centre; outside, or no radius, stays.
|
||||
{
|
||||
const float c[3] = { 0.0f, 0.0f, 0.0f };
|
||||
float p[3] = { 0.0f, 1.0f, 0.0f };
|
||||
CHECK(dent_push(p, c, 3.0f));
|
||||
CHECK(near(p[0], 0.0f) && near(p[1], 3.0f, 1e-3f) && near(p[2], 0.0f));
|
||||
float q[3] = { 4.0f, 0.0f, 0.0f };
|
||||
CHECK(!dent_push(q, c, 3.0f));
|
||||
CHECK(q[0] == 4.0f && q[1] == 0.0f && q[2] == 0.0f);
|
||||
float z[3] = { 0.5f, 0.0f, 0.0f };
|
||||
CHECK(!dent_push(z, c, 0.0f) && z[0] == 0.5f);
|
||||
float centre_pt[3] = { 0.0f, 0.0f, 0.0f }; // on the centre: finite
|
||||
dent_push(centre_pt, c, 3.0f);
|
||||
CHECK(std::isfinite(centre_pt[0]) && std::isfinite(centre_pt[1]));
|
||||
}
|
||||
// Weights and deform: offsets weighted, the cleft uncut, dents after.
|
||||
{
|
||||
RearShape s;
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
s.cheek[i].centre[0] = i ? 20.0f : -20.0f;
|
||||
s.cheek[i].centre[1] = 20.0f;
|
||||
s.cheek[i].centre[2] = -25.0f;
|
||||
for (float& r : s.cheek[i].radius)
|
||||
r = 25.0f;
|
||||
s.cheek[i].core = 0.5f;
|
||||
}
|
||||
s.cleft.centre[1] = 20.0f;
|
||||
s.cleft.centre[2] = -25.0f;
|
||||
s.cleft.radius[0] = 6.0f;
|
||||
s.cleft.radius[1] = s.cleft.radius[2] = 25.0f;
|
||||
s.cleft.core = 0.3f;
|
||||
s.seam_x = 0.0f;
|
||||
s.seam_soft = 3.0f;
|
||||
const float left_peak[3] = { -20.0f, 30.0f, -25.0f };
|
||||
RearWeights w = rear_weights(s, left_peak);
|
||||
CHECK(near(w.cheek[0], 1.0f) && w.cheek[1] == 0.0f && w.cleft == 0.0f);
|
||||
const float mid[3] = { 0.0f, 20.0f, -25.0f };
|
||||
w = rear_weights(s, mid);
|
||||
CHECK(near(w.cheek[0], w.cheek[1]) && w.cheek[0] > 0.0f && w.cheek[0] <= 0.5f + 1e-4f);
|
||||
CHECK(near(w.cleft, 1.0f));
|
||||
const float far_[3] = { 0.0f, -60.0f, 40.0f }; // the front, the waist
|
||||
w = rear_weights(s, far_);
|
||||
CHECK(w.cheek[0] == 0.0f && w.cheek[1] == 0.0f && w.cleft == 0.0f);
|
||||
// Across the seam the other cheek's region does not reach.
|
||||
const float across[3] = { 6.0f, 20.0f, -25.0f };
|
||||
w = rear_weights(s, across);
|
||||
CHECK(w.cheek[0] == 0.0f && w.cheek[1] > 0.5f);
|
||||
|
||||
const float offset[2][3] = { { 1.0f, 2.0f, 3.0f }, { -4.0f, 5.0f, -6.0f } };
|
||||
const float cleft[3] = { 0.0f, -2.0f, 0.0f };
|
||||
float out[3];
|
||||
RearWeights none;
|
||||
rear_deform(left_peak, none, offset, cleft, nullptr, 0, out);
|
||||
CHECK(out[0] == left_peak[0] && out[1] == left_peak[1] && out[2] == left_peak[2]);
|
||||
RearWeights one;
|
||||
one.cheek[0] = 1.0f;
|
||||
one.cleft = 0.5f;
|
||||
rear_deform(left_peak, one, offset, cleft, nullptr, 0, out);
|
||||
CHECK(near(out[0], -19.0f) && near(out[1], 31.0f) && near(out[2], -22.0f));
|
||||
RearDentSphere dent[2];
|
||||
dent[1].on = true;
|
||||
dent[1].centre[0] = -19.0f; dent[1].centre[1] = 30.0f; dent[1].centre[2] = -22.0f;
|
||||
dent[1].radius = 3.0f;
|
||||
rear_deform(left_peak, one, offset, cleft, dent, 2, out); // after the offsets
|
||||
CHECK(near(out[1], 33.0f, 1e-3f) && near(out[0], -19.0f, 1e-3f));
|
||||
dent[1].on = false;
|
||||
rear_deform(left_peak, one, offset, cleft, dent, 2, out);
|
||||
CHECK(near(out[1], 31.0f));
|
||||
}
|
||||
// Rounding to the source shorts.
|
||||
{
|
||||
CHECK(round_short(1.49f) == 1 && round_short(1.5f) == 2 && round_short(-1.6f) == -2);
|
||||
CHECK(round_short(40000.0f) == 32767 && round_short(-40000.0f) == -32768);
|
||||
CHECK(round_short(NAN) == 0);
|
||||
}
|
||||
// The fit, on a made-up pair of cheeks: spheres of 10 cm at x +-8 cm,
|
||||
// y 2 cm, z -9 cm (peaks at y 12 cm), a belt at z 0 sticking out further
|
||||
// back (ignored: above the window), and her front.
|
||||
{
|
||||
std::vector<float> pts;
|
||||
auto add = [&](float x, float y, float z)
|
||||
{
|
||||
pts.push_back(x * metre);
|
||||
pts.push_back(y * metre);
|
||||
pts.push_back(z * metre);
|
||||
};
|
||||
for (int side = 0; side < 2; ++side)
|
||||
{
|
||||
const float cx = side ? 0.08f : -0.08f;
|
||||
for (int a = 0; a < 24; ++a)
|
||||
for (int b = 0; b < 12; ++b)
|
||||
{
|
||||
const float th = 6.2831853f * (float)a / 24.0f;
|
||||
const float ph = 3.1415927f * ((float)b + 0.5f) / 12.0f;
|
||||
add(cx + 0.10f * std::sin(ph) * std::cos(th), 0.02f + 0.10f * std::sin(ph) * std::sin(th),
|
||||
-0.09f + 0.10f * std::cos(ph));
|
||||
}
|
||||
add(cx, 0.12f, -0.09f); // the very peak
|
||||
}
|
||||
for (int a = 0; a < 36; ++a) // the belt
|
||||
{
|
||||
const float th = 6.2831853f * (float)a / 36.0f;
|
||||
add(0.17f * std::cos(th), 0.2f * std::sin(th), 0.0f);
|
||||
}
|
||||
RearFit fit;
|
||||
const bool ok = rear_fit(pts.data(), (int)pts.size() / 3, metre, fit);
|
||||
CHECK(ok && fit.ok && !fit.fallback);
|
||||
CHECK(fit.samples[0] > 12 && fit.samples[1] > 12);
|
||||
CHECK(near(fit.peak[0][1], 0.12f * metre, 0.06f));
|
||||
CHECK(fit.peak[0][1] > 0.11f * metre && fit.peak[1][1] > 0.11f * metre);
|
||||
CHECK(fit.peak[0][0] < -0.05f * metre && fit.peak[1][0] > 0.05f * metre);
|
||||
CHECK(std::fabs(fit.peak[0][2] + 0.09f * metre) < 0.02f * metre);
|
||||
CHECK(fit.centre[0][0] < 0.0f && fit.centre[1][0] > 0.0f);
|
||||
CHECK(std::fabs(fit.centre[0][0] + fit.centre[1][0]) < 0.005f * metre); // symmetric
|
||||
CHECK(std::fabs(fit.seam_x) < 0.005f * metre);
|
||||
CHECK(fit.centre[0][1] < fit.peak[0][1]); // inside the skin
|
||||
for (int s = 0; s < 2; ++s)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
CHECK(fit.radius[s][k] >= 0.05f * metre - 0.01f && fit.radius[s][k] <= 0.15f * metre + 0.01f);
|
||||
// At the default sliders the peak moves fully, her front not at all.
|
||||
RearAdjust adj;
|
||||
RearShape shape;
|
||||
rear_shape_from_fit(fit, adj, metre, shape);
|
||||
RearWeights w = rear_weights(shape, fit.peak[1]);
|
||||
CHECK(w.cheek[1] > 0.95f && w.cheek[0] == 0.0f);
|
||||
const float front[3] = { 0.08f * metre, -0.08f * metre, -0.09f * metre };
|
||||
w = rear_weights(shape, front);
|
||||
CHECK(w.cheek[0] == 0.0f && w.cheek[1] == 0.0f);
|
||||
const float waist[3] = { 0.08f * metre, 0.08f * metre, 0.12f * metre };
|
||||
w = rear_weights(shape, waist);
|
||||
CHECK(w.cheek[1] == 0.0f);
|
||||
// Spread and lift move it from where it was fitted; 8 cm = as fitted.
|
||||
CHECK(near(shape.cheek[1].centre[0], fit.centre[1][0]));
|
||||
adj.spread_cm = 10.0f;
|
||||
adj.lift_cm = -1.0f;
|
||||
adj.size = 2.0f;
|
||||
adj.width = 0.5f;
|
||||
RearShape moved;
|
||||
rear_shape_from_fit(fit, adj, metre, moved);
|
||||
CHECK(near(moved.cheek[1].centre[0], fit.centre[1][0] + 0.02f * metre));
|
||||
CHECK(near(moved.cheek[0].centre[0], fit.centre[0][0] - 0.02f * metre));
|
||||
CHECK(near(moved.cheek[0].centre[2], fit.centre[0][2] - 0.01f * metre));
|
||||
CHECK(near(moved.cheek[0].radius[0], fit.radius[0][0]) &&
|
||||
near(moved.cheek[0].radius[1], 2.0f * fit.radius[0][1]));
|
||||
CHECK(near(moved.cleft.radius[0], 0.02f * metre) && near(moved.cleft.centre[0], fit.seam_x));
|
||||
// The touch zone holds the peak, its back edge a little past it.
|
||||
float zc[3], zs[3], depth = 0.0f, n[3];
|
||||
rear_touch_zone(fit, RearAdjust(), 1, metre, 0.0f, zc, zs);
|
||||
CHECK(ellipsoid_contact(fit.peak[1], zc, zs, 0.0f, &depth, n));
|
||||
const float behind[3] = { fit.peak[1][0], fit.peak[1][1] + 0.02f * metre, fit.peak[1][2] };
|
||||
CHECK(!ellipsoid_contact(behind, zc, zs, 0.0f, &depth, n));
|
||||
float zs2[3], zc2[3];
|
||||
rear_touch_zone(fit, RearAdjust(), 1, metre, 2.0f, zc2, zs2); // grown 2 cm
|
||||
CHECK(ellipsoid_contact(behind, zc2, zs2, 0.0f, &depth, n) && n[1] > 0.9f);
|
||||
}
|
||||
// No mesh to fit: the nominal shape, said so.
|
||||
{
|
||||
RearFit fit;
|
||||
CHECK(!rear_fit(nullptr, 0, metre, fit));
|
||||
CHECK(fit.fallback && !fit.ok);
|
||||
CHECK(fit.centre[0][0] < 0.0f && fit.centre[1][0] > 0.0f && fit.radius[0][1] > 0.0f);
|
||||
const float few[6] = { 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f };
|
||||
CHECK(!rear_fit(few, 2, metre, fit) && fit.fallback);
|
||||
}
|
||||
// Ellipsoid contact: depth and outward normal.
|
||||
{
|
||||
const float c[3] = { 0.0f, 0.0f, 0.0f };
|
||||
const float s[3] = { 10.0f, 20.0f, 30.0f };
|
||||
float depth = 0.0f, n[3];
|
||||
const float p[3] = { 0.0f, 18.0f, 0.0f };
|
||||
CHECK(ellipsoid_contact(p, c, s, 0.0f, &depth, n));
|
||||
CHECK(near(n[1], 1.0f) && near(depth, 2.1111f, 0.01f)); // (1 - 0.81) / (2 x 18/400)
|
||||
CHECK(ellipsoid_contact(c, c, s, 0.0f, &depth, n) && near(depth, 10.0f));
|
||||
const float q[3] = { 11.0f, 0.0f, 0.0f };
|
||||
CHECK(!ellipsoid_contact(q, c, s, 0.0f, &depth, n));
|
||||
CHECK(ellipsoid_contact(q, c, s, 2.0f, &depth, n) && near(n[0], 1.0f));
|
||||
}
|
||||
// The pelvis frame: round trips through a turned, scaled matrix.
|
||||
{
|
||||
// 90 deg about z, scale 2, at (5, 6, 7): local x -> world +y.
|
||||
const float m[16] = { 0, 2, 0, 0, -2, 0, 0, 0, 0, 0, 2, 0, 5, 6, 7, 1 };
|
||||
const float local[3] = { 1.0f, 2.0f, 3.0f };
|
||||
float w[3], back[3];
|
||||
local_point_to_world(m, local, w);
|
||||
CHECK(near(w[0], 1.0f) && near(w[1], 8.0f) && near(w[2], 13.0f));
|
||||
CHECK(world_point_to_local(m, w, back));
|
||||
CHECK(near(back[0], 1.0f) && near(back[1], 2.0f) && near(back[2], 3.0f));
|
||||
const float dir[3] = { 0.0f, 4.0f, 0.0f }; // world +y = local +x, scale 2
|
||||
CHECK(world_dir_to_local(m, dir, back) && near(back[0], 2.0f) && near(back[1], 0.0f));
|
||||
float d2[3];
|
||||
local_dir_to_world(m, back, d2);
|
||||
CHECK(near(d2[1], 4.0f) && near(d2[0], 0.0f));
|
||||
CHECK(near(matrix_scale(m), 2.0f));
|
||||
const float zero[16] = {};
|
||||
CHECK(!world_dir_to_local(zero, dir, back) && matrix_scale(zero) == 0.0f);
|
||||
}
|
||||
// Baseline fullness: outward from the cheek's centre by weight x
|
||||
// amount, straight back at the centre, nothing outside the region or
|
||||
// on the cleft, in (negative) the other way.
|
||||
{
|
||||
RearShape s;
|
||||
s.cheek[0].centre[0] = -8.0f;
|
||||
s.cheek[0].radius[0] = s.cheek[0].radius[1] = s.cheek[0].radius[2] = 10.0f;
|
||||
s.cheek[1] = s.cheek[0];
|
||||
s.cheek[1].centre[0] = 8.0f;
|
||||
s.cleft.radius[0] = s.cleft.radius[1] = s.cleft.radius[2] = 1.0f;
|
||||
RearWeights w;
|
||||
float delta[3] = {};
|
||||
const float centre[3] = { -8.0f, 0.0f, 0.0f };
|
||||
w.cheek[0] = 1.0f;
|
||||
rear_inflate(s, w, centre, 2.0f, delta);
|
||||
CHECK(near(delta[0], 0.0f) && near(delta[1], 2.0f) && near(delta[2], 0.0f));
|
||||
float d2[3] = {};
|
||||
const float side[3] = { -8.0f - 9.0f, 0.0f, 0.0f };
|
||||
rear_inflate(s, w, side, 2.0f, d2);
|
||||
CHECK(d2[0] < -0.5f && d2[1] > 0.0f); // out to her side, a little back
|
||||
float d3[3] = {};
|
||||
w.cheek[0] = 0.5f;
|
||||
rear_inflate(s, w, centre, 2.0f, d3);
|
||||
CHECK(near(d3[1], 1.0f)); // scaled by the weight
|
||||
float d4[3] = {};
|
||||
w.cheek[0] = 0.0f;
|
||||
w.cleft = 1.0f;
|
||||
rear_inflate(s, w, centre, 2.0f, d4);
|
||||
CHECK(d4[0] == 0.0f && d4[1] == 0.0f && d4[2] == 0.0f);
|
||||
float d5[3] = {};
|
||||
w.cheek[0] = 1.0f;
|
||||
rear_inflate(s, w, centre, -2.0f, d5);
|
||||
CHECK(near(d5[1], -2.0f));
|
||||
float d6[3] = {};
|
||||
rear_inflate(s, w, centre, 0.0f, d6);
|
||||
CHECK(d6[1] == 0.0f);
|
||||
}
|
||||
// The absolute limit: limit x tanh(len / limit) -- nearly untouched when
|
||||
// small, never above the limit however far it is pulled, still growing
|
||||
// past it (no wall), direction kept, off at 0.
|
||||
{
|
||||
float s[3] = { 0.3f, 0.4f, 0.0f }; // length 0.5, limit 10
|
||||
soft_limit_vector(s, 10.0f);
|
||||
CHECK(near(s[0], 0.3f, 0.002f) && near(s[1], 0.4f, 0.002f));
|
||||
float a[3] = { 3.0f, 4.0f, 0.0f }; // length 5: 10 tanh(0.5)
|
||||
soft_limit_vector(a, 10.0f);
|
||||
CHECK(near(std::sqrt(a[0] * a[0] + a[1] * a[1]), 10.0f * std::tanh(0.5f)) &&
|
||||
near(a[0] / a[1], 0.75f));
|
||||
float f[3] = { 15.0f, 0.0f, 0.0f }, g[3] = { 20.0f, 0.0f, 0.0f };
|
||||
soft_limit_vector(f, 10.0f);
|
||||
soft_limit_vector(g, 10.0f);
|
||||
CHECK(g[0] > f[0] + 0.3f && g[0] < 10.0f); // past the limit it still moves
|
||||
float b[3] = { 0.0f, 0.0f, 1000.0f };
|
||||
soft_limit_vector(b, 10.0f);
|
||||
CHECK(b[2] <= 10.0f && b[2] > 9.9f && b[0] == 0.0f);
|
||||
float c[3] = { 6.0f, 8.0f, 0.0f }; // length 10: on the limit
|
||||
soft_limit_vector(c, 10.0f);
|
||||
const float len = std::sqrt(c[0] * c[0] + c[1] * c[1]);
|
||||
CHECK(near(len, 10.0f * std::tanh(1.0f)) && near(c[0] / c[1], 0.75f));
|
||||
float d[3] = { 50.0f, 0.0f, 0.0f };
|
||||
soft_limit_vector(d, 0.0f);
|
||||
CHECK(near(d[0], 50.0f));
|
||||
float e[3] = { NAN, 1.0f, 0.0f };
|
||||
soft_limit_vector(e, 10.0f);
|
||||
CHECK(e[0] == 0.0f && e[1] == 0.0f);
|
||||
}
|
||||
// The shown offset: gains, eased toward 1 by a hand, inward scaled, the
|
||||
// test push added along +y.
|
||||
{
|
||||
const float gain[3] = { 0.5f, 1.0f, 0.0f };
|
||||
float out[3];
|
||||
const float lag[3] = { 2.0f, -4.0f, 3.0f };
|
||||
cheek_offset(lag, gain, 0.0f, 0.5f, 0.0f, out);
|
||||
CHECK(near(out[0], 1.0f) && near(out[1], -2.0f) && near(out[2], 0.0f));
|
||||
cheek_offset(lag, gain, 1.0f, 0.5f, 0.0f, out);
|
||||
CHECK(near(out[0], 2.0f) && near(out[1], -2.0f) && near(out[2], 3.0f));
|
||||
const float back[3] = { 0.0f, 4.0f, 0.0f };
|
||||
cheek_offset(back, gain, 0.0f, 0.0f, 10.0f, out);
|
||||
CHECK(near(out[1], 14.0f));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Tests for src/vr/soft_body, the contact part (included by mathtest.cpp).
|
||||
#include "../vr/physics/soft_body.h"
|
||||
|
||||
static void test_soft_body()
|
||||
{
|
||||
using namespace trlvr::rear;
|
||||
const float metre = 291.0f;
|
||||
const double dt = 1.0 / 60;
|
||||
|
||||
// A press records how far the hand has moved since it took hold, and
|
||||
// stops being updated once the hand is out.
|
||||
{
|
||||
RearContact sp;
|
||||
SoftContactState hand;
|
||||
SoftContactInput in;
|
||||
in.touching = true;
|
||||
in.hand = 1;
|
||||
in.metre = metre;
|
||||
in.normal[1] = -1.0f;
|
||||
in.point[1] = -12.0f;
|
||||
in.point[2] = 150.0f;
|
||||
double now = 1.0;
|
||||
bool first = false;
|
||||
for (int i = 0; i < 60; ++i)
|
||||
{
|
||||
now += dt;
|
||||
in.point[1] = -12.0f + 0.03f * metre * std::fmin(1.0f, i / 30.0f);
|
||||
in.now_seconds = now;
|
||||
if (soft_contact(sp, hand, in))
|
||||
first = true;
|
||||
}
|
||||
CHECK(first && hand.holding && !hand.grabbing);
|
||||
CHECK(near(sp.press[1][1], 0.03f * metre, 1e-3f) && sp.press[0][1] == 0.0f);
|
||||
CHECK(sp.press_time[1] == now && sp.grab_time[1] == 0.0);
|
||||
in.touching = false;
|
||||
in.point[1] += 5.0f;
|
||||
now += dt;
|
||||
in.now_seconds = now;
|
||||
soft_contact(sp, hand, in);
|
||||
CHECK(!hand.holding);
|
||||
CHECK(near(sp.press[1][1], 0.03f * metre, 1e-3f));
|
||||
}
|
||||
|
||||
// A fast arrival kicks once; a slow, outgoing or stale one does not.
|
||||
{
|
||||
const struct { float speed_m; double gap; float dir; bool kicks; } cases[] = {
|
||||
{ 1.0f, 0.016, +1.0f, true }, // into the surface at 1 m/s
|
||||
{ 0.1f, 0.016, +1.0f, false }, // too slow (< 0.2 m/s)
|
||||
{ 1.0f, 0.016, -1.0f, false }, // moving away from it
|
||||
{ 1.0f, 0.15, +1.0f, false }, // the last sample is too old
|
||||
{ 10.0f, 0.016, +1.0f, true }, // capped at 2.5 m/s
|
||||
};
|
||||
for (const auto& c : cases)
|
||||
{
|
||||
RearContact sp;
|
||||
SoftContactState hand;
|
||||
double now = 5.0;
|
||||
SoftContactInput in;
|
||||
in.hand = 0;
|
||||
in.metre = metre;
|
||||
in.normal[1] = -1.0f;
|
||||
in.point[1] = -0.1f * metre;
|
||||
in.now_seconds = now;
|
||||
soft_contact(sp, hand, in); // outside, seen
|
||||
in.touching = true;
|
||||
in.now_seconds = now + c.gap;
|
||||
in.point[1] = 0.0f;
|
||||
in.hand_velocity[1] = c.dir * c.speed_m * metre;
|
||||
soft_contact(sp, hand, in);
|
||||
CHECK(sp.kick_pending == c.kicks);
|
||||
if (c.kicks)
|
||||
{
|
||||
const float v = c.speed_m > 2.5f ? 2.5f : c.speed_m;
|
||||
CHECK(near(sp.kick[1], 0.6f * v * metre, 1e-3f));
|
||||
}
|
||||
else
|
||||
CHECK(sp.kick[1] == 0.0f);
|
||||
}
|
||||
// Staying in does not kick again.
|
||||
RearContact sp;
|
||||
SoftContactState hand;
|
||||
SoftContactInput in;
|
||||
in.metre = metre;
|
||||
in.normal[1] = -1.0f;
|
||||
in.point[1] = -0.1f * metre;
|
||||
in.now_seconds = 2.0;
|
||||
soft_contact(sp, hand, in);
|
||||
in.touching = true;
|
||||
in.now_seconds = 2.016;
|
||||
in.hand_velocity[1] = 1.0f * metre;
|
||||
soft_contact(sp, hand, in);
|
||||
const float once = sp.kick[1];
|
||||
in.now_seconds = 2.032;
|
||||
soft_contact(sp, hand, in);
|
||||
CHECK(once > 0.0f && sp.kick[1] == once);
|
||||
// No valid velocity: no kick.
|
||||
RearContact blind;
|
||||
SoftContactState h2;
|
||||
SoftContactInput bi;
|
||||
bi.metre = metre;
|
||||
bi.normal[1] = -1.0f;
|
||||
bi.now_seconds = 3.0;
|
||||
soft_contact(blind, h2, bi);
|
||||
bi.touching = true;
|
||||
bi.velocity_valid = false;
|
||||
bi.hand_velocity[1] = metre;
|
||||
bi.now_seconds = 3.016;
|
||||
soft_contact(blind, h2, bi);
|
||||
CHECK(!blind.kick_pending);
|
||||
}
|
||||
|
||||
// A grip holds the part even with the hand out of her body, until it opens.
|
||||
{
|
||||
RearContact sp;
|
||||
SoftContactState hand;
|
||||
double now = 1.0;
|
||||
SoftContactInput in;
|
||||
in.hand = 0;
|
||||
in.metre = metre;
|
||||
in.normal[1] = -1.0f;
|
||||
in.touching = true;
|
||||
in.gripping = true;
|
||||
for (int i = 0; i < 150; ++i)
|
||||
{
|
||||
now += dt;
|
||||
in.now_seconds = now;
|
||||
in.point[0] = 0.03f * metre * std::fmin(1.0f, i / 60.0f);
|
||||
if (i > 60)
|
||||
in.touching = false; // beyond her surface, grip still shut
|
||||
soft_contact(sp, hand, in);
|
||||
}
|
||||
CHECK(hand.holding && hand.grabbing);
|
||||
CHECK(near(sp.press[0][0], 0.03f * metre, 1e-3f));
|
||||
CHECK(sp.grab_time[0] == now);
|
||||
in.gripping = false;
|
||||
now += dt;
|
||||
in.now_seconds = now;
|
||||
soft_contact(sp, hand, in);
|
||||
CHECK(!hand.holding && !hand.grabbing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
// OpenXR probe: what does the active 32-bit runtime offer?
|
||||
//
|
||||
// Standalone, no game and no D3D9. Creates an instance, lists extensions,
|
||||
// asks for the system and the recommended eye size, then brings up a bare
|
||||
// Vulkan device the way XR_KHR_vulkan_enable wants it and creates a session
|
||||
// to read the swapchain formats, the real FOV and the interaction profile.
|
||||
// Every step logs and carries on where it can, so a partial result still
|
||||
// says how far the runtime got.
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
|
||||
#define VK_NO_PROTOTYPES
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#define XR_USE_GRAPHICS_API_VULKAN
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
XrInstance g_instance = XR_NULL_HANDLE;
|
||||
|
||||
const char* result_name(XrResult r)
|
||||
{
|
||||
static char buffer[XR_MAX_RESULT_STRING_SIZE];
|
||||
if (g_instance && XR_SUCCEEDED(xrResultToString(g_instance, r, buffer)))
|
||||
return buffer;
|
||||
snprintf(buffer, sizeof(buffer), "%d", (int)r);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
bool check(XrResult r, const char* what)
|
||||
{
|
||||
if (XR_SUCCEEDED(r))
|
||||
return true;
|
||||
printf("FAILED %s: %s\n", what, result_name(r));
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<const char*> split(std::string& list)
|
||||
{
|
||||
std::vector<const char*> out;
|
||||
char* p = list.data();
|
||||
while (p && *p)
|
||||
{
|
||||
while (*p == ' ') *p++ = 0;
|
||||
if (!*p) break;
|
||||
out.push_back(p);
|
||||
p = strchr(p, ' ');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
Fn vk_proc(HMODULE vk, const char* name)
|
||||
{
|
||||
return (Fn)GetProcAddress(vk, name);
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
uint32_t count = 0;
|
||||
if (!check(xrEnumerateInstanceExtensionProperties(nullptr, 0, &count, nullptr),
|
||||
"xrEnumerateInstanceExtensionProperties"))
|
||||
return 1;
|
||||
std::vector<XrExtensionProperties> exts(count, {XR_TYPE_EXTENSION_PROPERTIES});
|
||||
xrEnumerateInstanceExtensionProperties(nullptr, count, &count, exts.data());
|
||||
|
||||
bool have_vk1 = false, have_vk2 = false, have_hp = false;
|
||||
printf("extensions (%u):\n", count);
|
||||
for (const auto& e : exts)
|
||||
{
|
||||
printf(" %s v%u\n", e.extensionName, e.extensionVersion);
|
||||
if (!strcmp(e.extensionName, XR_KHR_VULKAN_ENABLE_EXTENSION_NAME)) have_vk1 = true;
|
||||
if (!strcmp(e.extensionName, XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME)) have_vk2 = true;
|
||||
if (!strcmp(e.extensionName, XR_EXT_HP_MIXED_REALITY_CONTROLLER_EXTENSION_NAME)) have_hp = true;
|
||||
}
|
||||
printf("vulkan_enable=%d vulkan_enable2=%d hp_controller=%d\n", have_vk1, have_vk2, have_hp);
|
||||
if (!have_vk1)
|
||||
{
|
||||
printf("no XR_KHR_vulkan_enable; stopping\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* enabled[] = {XR_KHR_VULKAN_ENABLE_EXTENSION_NAME};
|
||||
XrInstanceCreateInfo ici{XR_TYPE_INSTANCE_CREATE_INFO};
|
||||
strcpy(ici.applicationInfo.applicationName, "trlvr_xrprobe");
|
||||
ici.applicationInfo.apiVersion = XR_MAKE_VERSION(1, 0, 0);
|
||||
ici.enabledExtensionCount = 1;
|
||||
ici.enabledExtensionNames = enabled;
|
||||
if (!check(xrCreateInstance(&ici, &g_instance), "xrCreateInstance"))
|
||||
return 1;
|
||||
|
||||
XrInstanceProperties ip{XR_TYPE_INSTANCE_PROPERTIES};
|
||||
if (check(xrGetInstanceProperties(g_instance, &ip), "xrGetInstanceProperties"))
|
||||
printf("runtime: %s %u.%u.%u\n", ip.runtimeName,
|
||||
XR_VERSION_MAJOR(ip.runtimeVersion), XR_VERSION_MINOR(ip.runtimeVersion),
|
||||
XR_VERSION_PATCH(ip.runtimeVersion));
|
||||
|
||||
XrSystemGetInfo sgi{XR_TYPE_SYSTEM_GET_INFO};
|
||||
sgi.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
|
||||
XrSystemId system = XR_NULL_SYSTEM_ID;
|
||||
if (!check(xrGetSystem(g_instance, &sgi, &system), "xrGetSystem"))
|
||||
return 1;
|
||||
|
||||
XrSystemProperties sp{XR_TYPE_SYSTEM_PROPERTIES};
|
||||
if (check(xrGetSystemProperties(g_instance, system, &sp), "xrGetSystemProperties"))
|
||||
printf("system: %s max swapchain %ux%u layers %u\n", sp.systemName,
|
||||
sp.graphicsProperties.maxSwapchainImageWidth,
|
||||
sp.graphicsProperties.maxSwapchainImageHeight,
|
||||
sp.graphicsProperties.maxLayerCount);
|
||||
|
||||
XrViewConfigurationView vcv[2] = {{XR_TYPE_VIEW_CONFIGURATION_VIEW},
|
||||
{XR_TYPE_VIEW_CONFIGURATION_VIEW}};
|
||||
if (check(xrEnumerateViewConfigurationViews(g_instance, system,
|
||||
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 2, &count, vcv),
|
||||
"xrEnumerateViewConfigurationViews"))
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
printf("view %u: recommended %ux%u max %ux%u samples %u\n", i,
|
||||
vcv[i].recommendedImageRectWidth, vcv[i].recommendedImageRectHeight,
|
||||
vcv[i].maxImageRectWidth, vcv[i].maxImageRectHeight,
|
||||
vcv[i].recommendedSwapchainSampleCount);
|
||||
|
||||
// Extension functions are not exported by the loader.
|
||||
PFN_xrGetVulkanGraphicsRequirementsKHR xrGetVulkanGraphicsRequirementsKHR = nullptr;
|
||||
PFN_xrGetVulkanInstanceExtensionsKHR xrGetVulkanInstanceExtensionsKHR = nullptr;
|
||||
PFN_xrGetVulkanDeviceExtensionsKHR xrGetVulkanDeviceExtensionsKHR = nullptr;
|
||||
PFN_xrGetVulkanGraphicsDeviceKHR xrGetVulkanGraphicsDeviceKHR = nullptr;
|
||||
xrGetInstanceProcAddr(g_instance, "xrGetVulkanGraphicsRequirementsKHR", (PFN_xrVoidFunction*)&xrGetVulkanGraphicsRequirementsKHR);
|
||||
xrGetInstanceProcAddr(g_instance, "xrGetVulkanInstanceExtensionsKHR", (PFN_xrVoidFunction*)&xrGetVulkanInstanceExtensionsKHR);
|
||||
xrGetInstanceProcAddr(g_instance, "xrGetVulkanDeviceExtensionsKHR", (PFN_xrVoidFunction*)&xrGetVulkanDeviceExtensionsKHR);
|
||||
xrGetInstanceProcAddr(g_instance, "xrGetVulkanGraphicsDeviceKHR", (PFN_xrVoidFunction*)&xrGetVulkanGraphicsDeviceKHR);
|
||||
if (!xrGetVulkanGraphicsRequirementsKHR || !xrGetVulkanInstanceExtensionsKHR ||
|
||||
!xrGetVulkanDeviceExtensionsKHR || !xrGetVulkanGraphicsDeviceKHR)
|
||||
{
|
||||
printf("XR_KHR_vulkan_enable entry points missing\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
XrGraphicsRequirementsVulkanKHR req{XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR};
|
||||
if (check(xrGetVulkanGraphicsRequirementsKHR(g_instance, system, &req),
|
||||
"xrGetVulkanGraphicsRequirementsKHR"))
|
||||
printf("vulkan api: min %u.%u max %u.%u\n",
|
||||
XR_VERSION_MAJOR(req.minApiVersionSupported), XR_VERSION_MINOR(req.minApiVersionSupported),
|
||||
XR_VERSION_MAJOR(req.maxApiVersionSupported), XR_VERSION_MINOR(req.maxApiVersionSupported));
|
||||
|
||||
std::string inst_exts, dev_exts;
|
||||
if (check(xrGetVulkanInstanceExtensionsKHR(g_instance, system, 0, &count, nullptr),
|
||||
"xrGetVulkanInstanceExtensionsKHR"))
|
||||
{
|
||||
inst_exts.resize(count);
|
||||
xrGetVulkanInstanceExtensionsKHR(g_instance, system, count, &count, inst_exts.data());
|
||||
printf("vulkan instance extensions: %s\n", inst_exts.c_str());
|
||||
}
|
||||
if (check(xrGetVulkanDeviceExtensionsKHR(g_instance, system, 0, &count, nullptr),
|
||||
"xrGetVulkanDeviceExtensionsKHR"))
|
||||
{
|
||||
dev_exts.resize(count);
|
||||
xrGetVulkanDeviceExtensionsKHR(g_instance, system, count, &count, dev_exts.data());
|
||||
printf("vulkan device extensions: %s\n", dev_exts.c_str());
|
||||
}
|
||||
|
||||
// A bare Vulkan device, enough to create a session with.
|
||||
HMODULE vk = LoadLibraryA("vulkan-1.dll");
|
||||
if (!vk)
|
||||
{
|
||||
printf("vulkan-1.dll not found; stopping before the session\n");
|
||||
return 0;
|
||||
}
|
||||
auto vkCreateInstance = vk_proc<PFN_vkCreateInstance>(vk, "vkCreateInstance");
|
||||
auto vkGetPhysicalDeviceProperties = vk_proc<PFN_vkGetPhysicalDeviceProperties>(vk, "vkGetPhysicalDeviceProperties");
|
||||
auto vkGetPhysicalDeviceQueueFamilyProperties = vk_proc<PFN_vkGetPhysicalDeviceQueueFamilyProperties>(vk, "vkGetPhysicalDeviceQueueFamilyProperties");
|
||||
auto vkCreateDevice = vk_proc<PFN_vkCreateDevice>(vk, "vkCreateDevice");
|
||||
|
||||
auto inst_list = split(inst_exts);
|
||||
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
||||
app.apiVersion = VK_API_VERSION_1_3;
|
||||
VkInstanceCreateInfo vici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
|
||||
vici.pApplicationInfo = &app;
|
||||
vici.enabledExtensionCount = (uint32_t)inst_list.size();
|
||||
vici.ppEnabledExtensionNames = inst_list.data();
|
||||
VkInstance vk_instance = VK_NULL_HANDLE;
|
||||
VkResult vr = vkCreateInstance(&vici, nullptr, &vk_instance);
|
||||
if (vr != VK_SUCCESS)
|
||||
{
|
||||
printf("FAILED vkCreateInstance: %d\n", (int)vr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
VkPhysicalDevice phys = VK_NULL_HANDLE;
|
||||
if (!check(xrGetVulkanGraphicsDeviceKHR(g_instance, system, vk_instance, &phys),
|
||||
"xrGetVulkanGraphicsDeviceKHR"))
|
||||
return 1;
|
||||
VkPhysicalDeviceProperties props{};
|
||||
vkGetPhysicalDeviceProperties(phys, &props);
|
||||
printf("vulkan device: %s\n", props.deviceName);
|
||||
|
||||
uint32_t qcount = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(phys, &qcount, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> qprops(qcount);
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(phys, &qcount, qprops.data());
|
||||
uint32_t family = 0;
|
||||
for (uint32_t i = 0; i < qcount; ++i)
|
||||
if (qprops[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { family = i; break; }
|
||||
|
||||
auto dev_list = split(dev_exts);
|
||||
const float priority = 1.0f;
|
||||
VkDeviceQueueCreateInfo qci{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
qci.queueFamilyIndex = family;
|
||||
qci.queueCount = 1;
|
||||
qci.pQueuePriorities = &priority;
|
||||
VkDeviceCreateInfo dci{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
||||
dci.queueCreateInfoCount = 1;
|
||||
dci.pQueueCreateInfos = &qci;
|
||||
dci.enabledExtensionCount = (uint32_t)dev_list.size();
|
||||
dci.ppEnabledExtensionNames = dev_list.data();
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
vr = vkCreateDevice(phys, &dci, nullptr, &device);
|
||||
if (vr != VK_SUCCESS)
|
||||
{
|
||||
printf("FAILED vkCreateDevice: %d\n", (int)vr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
XrGraphicsBindingVulkanKHR binding{XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR};
|
||||
binding.instance = vk_instance;
|
||||
binding.physicalDevice = phys;
|
||||
binding.device = device;
|
||||
binding.queueFamilyIndex = family;
|
||||
binding.queueIndex = 0;
|
||||
XrSessionCreateInfo sci{XR_TYPE_SESSION_CREATE_INFO};
|
||||
sci.next = &binding;
|
||||
sci.systemId = system;
|
||||
XrSession session = XR_NULL_HANDLE;
|
||||
if (!check(xrCreateSession(g_instance, &sci, &session), "xrCreateSession"))
|
||||
return 1;
|
||||
printf("session created with XrGraphicsBindingVulkanKHR\n");
|
||||
|
||||
if (check(xrEnumerateSwapchainFormats(session, 0, &count, nullptr), "xrEnumerateSwapchainFormats"))
|
||||
{
|
||||
std::vector<int64_t> formats(count);
|
||||
xrEnumerateSwapchainFormats(session, count, &count, formats.data());
|
||||
printf("swapchain formats (VkFormat):");
|
||||
for (int64_t f : formats) printf(" %lld", (long long)f);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
if (check(xrEnumerateReferenceSpaces(session, 0, &count, nullptr), "xrEnumerateReferenceSpaces"))
|
||||
{
|
||||
std::vector<XrReferenceSpaceType> spaces(count);
|
||||
xrEnumerateReferenceSpaces(session, count, &count, spaces.data());
|
||||
printf("reference spaces:");
|
||||
for (auto s : spaces) printf(" %d", (int)s);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
// A double-wide swapchain, as the side-by-side submit wants.
|
||||
{
|
||||
XrSwapchainCreateInfo sw{XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
||||
sw.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT |
|
||||
XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT |
|
||||
XR_SWAPCHAIN_USAGE_SAMPLED_BIT;
|
||||
sw.format = VK_FORMAT_B8G8R8A8_SRGB;
|
||||
sw.sampleCount = 1;
|
||||
sw.width = vcv[0].recommendedImageRectWidth * 2;
|
||||
sw.height = vcv[0].recommendedImageRectHeight;
|
||||
sw.faceCount = 1;
|
||||
sw.arraySize = 1;
|
||||
sw.mipCount = 1;
|
||||
XrSwapchain chain = XR_NULL_HANDLE;
|
||||
if (check(xrCreateSwapchain(session, &sw, &chain), "xrCreateSwapchain (double wide, BGRA8 sRGB)"))
|
||||
{
|
||||
xrEnumerateSwapchainImages(chain, 0, &count, nullptr);
|
||||
printf("double-wide swapchain %ux%u ok, %u images\n", sw.width, sw.height, count);
|
||||
xrDestroySwapchain(chain);
|
||||
}
|
||||
}
|
||||
|
||||
XrReferenceSpaceCreateInfo rsci{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||
rsci.poseInReferenceSpace.orientation.w = 1.0f;
|
||||
rsci.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
|
||||
XrSpace view_space = XR_NULL_HANDLE;
|
||||
check(xrCreateReferenceSpace(session, &rsci, &view_space), "xrCreateReferenceSpace(VIEW)");
|
||||
|
||||
// Wait for the session to become ready, then run a few empty frames so
|
||||
// xrLocateViews has a real display time to answer for.
|
||||
bool running = false;
|
||||
int frames = 0;
|
||||
const DWORD start = GetTickCount();
|
||||
while (GetTickCount() - start < 15000 && frames < 20)
|
||||
{
|
||||
XrEventDataBuffer ev{XR_TYPE_EVENT_DATA_BUFFER};
|
||||
while (xrPollEvent(g_instance, &ev) == XR_SUCCESS)
|
||||
{
|
||||
if (ev.type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED)
|
||||
{
|
||||
const auto* sc = (const XrEventDataSessionStateChanged*)&ev;
|
||||
printf("session state -> %d\n", (int)sc->state);
|
||||
if (sc->state == XR_SESSION_STATE_READY)
|
||||
{
|
||||
XrSessionBeginInfo bi{XR_TYPE_SESSION_BEGIN_INFO};
|
||||
bi.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
running = check(xrBeginSession(session, &bi), "xrBeginSession");
|
||||
}
|
||||
else if (sc->state == XR_SESSION_STATE_STOPPING)
|
||||
{
|
||||
xrEndSession(session);
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
ev = {XR_TYPE_EVENT_DATA_BUFFER};
|
||||
}
|
||||
if (!running)
|
||||
{
|
||||
Sleep(50);
|
||||
continue;
|
||||
}
|
||||
|
||||
XrFrameState fs{XR_TYPE_FRAME_STATE};
|
||||
if (!check(xrWaitFrame(session, nullptr, &fs), "xrWaitFrame")) break;
|
||||
if (!check(xrBeginFrame(session, nullptr), "xrBeginFrame")) break;
|
||||
|
||||
if (frames == 10 && view_space)
|
||||
{
|
||||
XrViewLocateInfo vli{XR_TYPE_VIEW_LOCATE_INFO};
|
||||
vli.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
vli.displayTime = fs.predictedDisplayTime;
|
||||
vli.space = view_space;
|
||||
XrViewState vs{XR_TYPE_VIEW_STATE};
|
||||
XrView views[2] = {{XR_TYPE_VIEW}, {XR_TYPE_VIEW}};
|
||||
if (check(xrLocateViews(session, &vli, &vs, 2, &count, views), "xrLocateViews"))
|
||||
{
|
||||
printf("view state flags 0x%llx, period %lld ns\n",
|
||||
(unsigned long long)vs.viewStateFlags, (long long)fs.predictedDisplayPeriod);
|
||||
for (int i = 0; i < 2; ++i)
|
||||
printf("eye %d: tan l %.4f r %.4f u %.4f d %.4f pos %.4f %.4f %.4f quat %.4f %.4f %.4f %.4f\n", i,
|
||||
tanf(views[i].fov.angleLeft), tanf(views[i].fov.angleRight),
|
||||
tanf(views[i].fov.angleUp), tanf(views[i].fov.angleDown),
|
||||
views[i].pose.position.x, views[i].pose.position.y, views[i].pose.position.z,
|
||||
views[i].pose.orientation.x, views[i].pose.orientation.y,
|
||||
views[i].pose.orientation.z, views[i].pose.orientation.w);
|
||||
}
|
||||
}
|
||||
|
||||
XrFrameEndInfo fei{XR_TYPE_FRAME_END_INFO};
|
||||
fei.displayTime = fs.predictedDisplayTime;
|
||||
fei.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
if (!check(xrEndFrame(session, &fei), "xrEndFrame")) break;
|
||||
++frames;
|
||||
}
|
||||
printf("frames run: %d%s\n", frames, running ? "" : " (session never became ready -- headset connected?)");
|
||||
|
||||
xrDestroySession(session);
|
||||
xrDestroyInstance(g_instance);
|
||||
return 0;
|
||||
}
|
||||
+4260
-587
File diff suppressed because it is too large.
Load diff
+25
-4
@@ -44,11 +44,20 @@ namespace trlvr
|
||||
// called every frame keeps it up. For the radial gear menu.
|
||||
void camera_show_retail_gear_cross();
|
||||
// The chosen view: first_person in trlvr.ini at start, switched live
|
||||
// (user request 2026-10-06) by camera_toggle_view -- right stick long
|
||||
// press with immersive controls, or \ on the keyboard. First person is
|
||||
// only *active* in gameplay; this is what the player picked.
|
||||
// (user request 2026-10-06) by camera_toggle_view -- the right stick's
|
||||
// long press with immersive controls, or \ on the keyboard. First person is only
|
||||
// *active* in gameplay; this
|
||||
// is what the player picked.
|
||||
bool camera_view_first_person();
|
||||
void camera_toggle_view(const char* why);
|
||||
// A view switch is still gliding (or about to become first person).
|
||||
bool camera_view_tween_active();
|
||||
// Third person: that controller hand is on Lara's body right now (its
|
||||
// trigger and grip then belong to the touch, not the game).
|
||||
bool camera_hand_touching_lara(bool left);
|
||||
// Each hand's grab this frame, for the third-person soft-part hold: the
|
||||
// grip button, or with [vr] god_grab_pose the hand's pinch / fist.
|
||||
void camera_set_hand_grips(bool left, bool right);
|
||||
// The third-person preset in use (0 classic, 1 shoulder, 2 board):
|
||||
// third_person_mode at start; with third_person_mode = all the view
|
||||
// switch steps through every preset and first person in turn.
|
||||
@@ -66,9 +75,17 @@ namespace trlvr
|
||||
bool camera_tp_view_available();
|
||||
float camera_tp_scale();
|
||||
float camera_tp_zoom();
|
||||
void camera_set_tp_view(float scale, float zoom, bool save);
|
||||
// Sideways seat offset, metres at Lara's size (+ right, - left).
|
||||
float camera_tp_side();
|
||||
void camera_set_tp_view(float scale, float zoom, float side, bool save);
|
||||
// The current preset's defaults (the palm sliders' notches).
|
||||
void camera_tp_defaults(float* scale, float* zoom, float* side);
|
||||
// How much a drawn third-person hand's palm faces up (-2 unknown).
|
||||
float camera_palm_up(int hand);
|
||||
// A third-person hand's index fingertip as drawn (Lara's bone, or the
|
||||
// index's last joint when the fingers are not posed), head frame, game
|
||||
// units. False in first person or when that hand was not drawn lately.
|
||||
bool camera_hand_index_tip_head(bool left, float out[3]);
|
||||
// Main camera to Lara after the head pose, world units (world-marker
|
||||
// stereo depth).
|
||||
float camera_marker_depth();
|
||||
@@ -111,6 +128,10 @@ namespace trlvr
|
||||
// pick Lara up, 2 held with floor below (landing spot), 3 held with no
|
||||
// floor found (release does nothing).
|
||||
int camera_board_pluck_ring(float points[24][3]);
|
||||
// body_rear_debug wireframes: head-space segments (x0 y0 z0 x1 y1 z1)
|
||||
// and a colour each; returns the count (0 when off or stale).
|
||||
int camera_rear_debug_lines(float (*seg)[6], unsigned* colours,
|
||||
int max_segments);
|
||||
// A cutscene is playing (retail cinematic or resident cinematic).
|
||||
bool camera_cinematic_playing();
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "finger_input.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const float kEaseSeconds = 0.05f;
|
||||
const float kMaxDt = 0.25f;
|
||||
const float kTriggerDeadzone = 0.05f;
|
||||
const float kMaxRestCurl = 0.6f;
|
||||
// A pull this far is a full one (worn or faulty controllers stop
|
||||
// short of 1); a pull past kPulledTouch means the finger is on it.
|
||||
const float kFullPress = 0.9f;
|
||||
const float kPulledTouch = 0.2f;
|
||||
|
||||
float full_press(float v)
|
||||
{
|
||||
v /= kFullPress;
|
||||
return v < 0.0f ? 0.0f : v > 1.0f ? 1.0f : v;
|
||||
}
|
||||
|
||||
float clamp_rest(float rest_curl)
|
||||
{
|
||||
if (rest_curl < 0.0f) return 0.0f;
|
||||
if (rest_curl > kMaxRestCurl) return kMaxRestCurl;
|
||||
return rest_curl;
|
||||
}
|
||||
}
|
||||
|
||||
void finger_curls_wanted(const FingerInput& input, float rest_curl,
|
||||
float curl_out[5], float spread_out[4])
|
||||
{
|
||||
const bool thumb_down = input.has_sensors ? input.touch_thumb : input.thumb_button;
|
||||
// A trigger pulled in counts as touched even if the sensor says
|
||||
// not: a sensor dropping out at full press would straighten the
|
||||
// finger and let go of her.
|
||||
const bool on_trigger = input.has_sensors
|
||||
? input.touch_trigger || input.trigger > kPulledTouch
|
||||
: input.trigger > kTriggerDeadzone;
|
||||
|
||||
const float rest = clamp_rest(rest_curl);
|
||||
const float grip = full_press(input.grip);
|
||||
|
||||
float thumb = thumb_down ? 0.55f + 0.1f * grip : 0.05f;
|
||||
float index = on_trigger ? 0.35f + 0.55f * full_press(input.trigger) : 0.0f;
|
||||
float middle = 0.9f * grip;
|
||||
float ring = middle;
|
||||
float pinky = middle;
|
||||
|
||||
// The rest floor applies to the four fingers, not the thumb.
|
||||
if (index < rest) index = rest;
|
||||
if (middle < rest) middle = rest;
|
||||
if (ring < rest) ring = rest;
|
||||
if (pinky < rest) pinky = rest;
|
||||
|
||||
curl_out[0] = thumb;
|
||||
curl_out[1] = index;
|
||||
curl_out[2] = middle;
|
||||
curl_out[3] = ring;
|
||||
curl_out[4] = pinky;
|
||||
|
||||
const float spread = 0.4f + 0.15f * (1.0f - grip);
|
||||
spread_out[0] = spread;
|
||||
spread_out[1] = spread;
|
||||
spread_out[2] = spread;
|
||||
spread_out[3] = spread;
|
||||
}
|
||||
|
||||
void finger_curls_step(FingerCurls& state, const FingerInput& input,
|
||||
float rest_curl, float dt)
|
||||
{
|
||||
float wanted_curl[5];
|
||||
float wanted_spread[4];
|
||||
finger_curls_wanted(input, rest_curl, wanted_curl, wanted_spread);
|
||||
|
||||
// dt 0 (a coarse clock that has not ticked): hold, do not snap.
|
||||
if (state.valid && dt == 0.0f)
|
||||
return;
|
||||
if (!state.valid || dt < 0.0f || dt > kMaxDt)
|
||||
{
|
||||
for (int i = 0; i < 5; ++i) state.curl[i] = wanted_curl[i];
|
||||
for (int i = 0; i < 4; ++i) state.spread[i] = wanted_spread[i];
|
||||
state.valid = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const float follow = 1.0f - std::exp(-dt / kEaseSeconds);
|
||||
for (int i = 0; i < 5; ++i)
|
||||
state.curl[i] += (wanted_curl[i] - state.curl[i]) * follow;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
state.spread[i] += (wanted_spread[i] - state.spread[i]) * follow;
|
||||
}
|
||||
|
||||
void finger_curls_reset(FingerCurls& state)
|
||||
{
|
||||
state.valid = false;
|
||||
for (int i = 0; i < 5; ++i) state.curl[i] = 0.0f;
|
||||
for (int i = 0; i < 4; ++i) state.spread[i] = 0.0f;
|
||||
}
|
||||
|
||||
HandGesture finger_gesture(const float curl[5])
|
||||
{
|
||||
const float thumb = curl[0], index = curl[1];
|
||||
const float others = (curl[2] + curl[3] + curl[4]) / 3.0f;
|
||||
const bool closed = others > 0.5f;
|
||||
if (index > 0.6f && closed)
|
||||
return HandGesture::Fist;
|
||||
if (index > 0.6f && thumb > 0.4f)
|
||||
return HandGesture::Pinch;
|
||||
if (index < 0.4f && closed)
|
||||
return HandGesture::Point;
|
||||
return HandGesture::Open;
|
||||
}
|
||||
|
||||
bool finger_grabbing(const float curl[5], bool was)
|
||||
{
|
||||
// Starting: as finger_gesture's pinch or fist. Holding on: a clearly
|
||||
// opened hand to let go, so a flickering trigger does not drop her.
|
||||
const float index_min = was ? 0.45f : 0.6f;
|
||||
const float others_min = was ? 0.35f : 0.5f;
|
||||
const float thumb_min = was ? 0.3f : 0.4f;
|
||||
const float others = (curl[2] + curl[3] + curl[4]) / 3.0f;
|
||||
return curl[1] > index_min && (others > others_min || curl[0] > thumb_min);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
// Finger curls for the VR hands (ported from the Underworld mod's
|
||||
// finger_input). Pure maths -- no config(), no game memory, no Windows calls
|
||||
// -- so mathtest links it.
|
||||
//
|
||||
// Per hand, per frame, the controller's readings become a wanted pose:
|
||||
// thumb_down = touch_thumb (sensors) or thumb_button (no sensors)
|
||||
// on_trigger = touch_trigger || trigger > 0.2 (sensors; a real pull
|
||||
// counts even if the sensor drops out at full press)
|
||||
// or trigger > 0.05 (no sensors)
|
||||
// thumb = thumb_down ? 0.55 + 0.1*grip : 0.05
|
||||
// index = on_trigger ? 0.35 + 0.55*trigger : 0
|
||||
// middle = ring = pinky = 0.9*grip
|
||||
// spread (index, middle, ring, pinky) = 0.4 + 0.15*(1 - grip)
|
||||
// trigger and grip are read as full at 0.9 (worn controllers stop short of
|
||||
// 1). Spread is a 0..1 amount of finger splay, not an angle; the hand model
|
||||
// maps it to an angle itself. rest_curl (0..0.6) is a floor: index, middle,
|
||||
// ring and pinky are raised to it, so a relaxed hand never looks fully flat.
|
||||
// The thumb is not floored.
|
||||
//
|
||||
// The displayed curls ease toward the wanted ones with a 50 ms time constant
|
||||
// (follow = 1 - exp(-dt / 0.05)). The first frame, dt <= 0 and dt > 0.25 snap
|
||||
// straight to the wanted values. Inputs are expected in 0..1.
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
struct FingerInput
|
||||
{
|
||||
float trigger = 0.0f; // index trigger pull, 0 .. 1
|
||||
float grip = 0.0f; // grip / squeeze, 0 .. 1
|
||||
bool has_sensors = false; // controller has capacitive touch sensors
|
||||
bool touch_trigger = false; // index finger resting on the trigger
|
||||
bool touch_thumb = false; // thumb on stick, face button or thumb rest
|
||||
bool thumb_button = false; // no sensors: any face button or stick click
|
||||
};
|
||||
|
||||
struct FingerCurls
|
||||
{
|
||||
bool valid = false;
|
||||
float curl[5] = {}; // thumb, index, middle, ring, pinky; 0 = open
|
||||
float spread[4] = {}; // index, middle, ring, pinky; 0..1 splay amount
|
||||
};
|
||||
|
||||
// Wanted curls and spreads for one frame, before easing. curl_out gets 5
|
||||
// values (thumb first), spread_out 4. rest_curl is clamped to 0 .. 0.6.
|
||||
void finger_curls_wanted(const FingerInput& input, float rest_curl,
|
||||
float curl_out[5], float spread_out[4]);
|
||||
|
||||
// Advances state one frame toward the wanted pose. dt in seconds. The
|
||||
// first call after a reset (or with dt <= 0 or dt > 0.25) snaps to the
|
||||
// wanted values and marks the state valid.
|
||||
void finger_curls_step(FingerCurls& state, const FingerInput& input,
|
||||
float rest_curl, float dt);
|
||||
|
||||
// Clears the state so the next step snaps.
|
||||
void finger_curls_reset(FingerCurls& state);
|
||||
|
||||
// The hand's gesture from its curls (thumb, index, middle, ring, pinky).
|
||||
// Point: index straight (< 0.4, i.e. not pulling the trigger) with the
|
||||
// other three curled (> 0.5, the grip). Fist: index pulled in (> 0.6)
|
||||
// and the other three curled. Pinch: index pulled in with the thumb down
|
||||
// (> 0.4) and the others open. Open: anything else. Grabbing (god mode)
|
||||
// = pinch or fist; open and point only press.
|
||||
enum class HandGesture : int { Open = 0, Point = 1, Pinch = 2, Fist = 3 };
|
||||
HandGesture finger_gesture(const float curl[5]);
|
||||
inline bool finger_gesture_grabs(HandGesture g)
|
||||
{
|
||||
return g == HandGesture::Pinch || g == HandGesture::Fist;
|
||||
}
|
||||
|
||||
// Grabbing with hysteresis: `was` = grabbing last frame. Starts where
|
||||
// finger_gesture_grabs does; once grabbing, lets go only when the index
|
||||
// opens below 0.45 or both the other fingers (0.35) and thumb (0.3) open.
|
||||
bool finger_grabbing(const float curl[5], bool was);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "board_leash.h"
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
void board_leash(float anchor[3], const float lara[3], float radius_units,
|
||||
float height_units)
|
||||
{
|
||||
const float dx = lara[0] - anchor[0], dy = lara[1] - anchor[1];
|
||||
const float dist = sqrtf(dx * dx + dy * dy);
|
||||
if (radius_units <= 0.0f)
|
||||
{
|
||||
anchor[0] = lara[0];
|
||||
anchor[1] = lara[1];
|
||||
}
|
||||
else if (dist > radius_units)
|
||||
{
|
||||
const float f = (dist - radius_units) / dist;
|
||||
anchor[0] += dx * f;
|
||||
anchor[1] += dy * f;
|
||||
}
|
||||
const float dz = lara[2] - anchor[2];
|
||||
if (height_units <= 0.0f)
|
||||
anchor[2] = lara[2];
|
||||
else if (dz > height_units)
|
||||
anchor[2] = lara[2] - height_units;
|
||||
else if (dz < -height_units)
|
||||
anchor[2] = lara[2] + height_units;
|
||||
}
|
||||
|
||||
float board_stick_axis(float v, float dead)
|
||||
{
|
||||
if (!(v == v))
|
||||
return 0.0f;
|
||||
if (dead < 0.0f)
|
||||
dead = 0.0f;
|
||||
if (dead >= 1.0f || fabsf(v) < dead)
|
||||
return 0.0f;
|
||||
const float t = (v - (v > 0.0f ? dead : -dead)) / (1.0f - dead);
|
||||
return t > 1.0f ? 1.0f : t < -1.0f ? -1.0f : t;
|
||||
}
|
||||
|
||||
bool board_stick_step(float* zoom, float* scale, float zoom_in,
|
||||
float scale_in, float zoom_speed, float scale_speed,
|
||||
float dt, float zoom_lo, float zoom_hi,
|
||||
float scale_lo, float scale_hi)
|
||||
{
|
||||
if (!zoom || !scale || !(dt > 0.0f) || !(dt < 0.5f))
|
||||
return false;
|
||||
bool changed = false;
|
||||
if (zoom_in != 0.0f && zoom_in == zoom_in && zoom_speed > 0.0f)
|
||||
{
|
||||
float z = *zoom * expf(-zoom_in * zoom_speed * dt);
|
||||
z = z < zoom_lo ? zoom_lo : z > zoom_hi ? zoom_hi : z;
|
||||
if (z == z && z != *zoom)
|
||||
{
|
||||
*zoom = z;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (scale_in != 0.0f && scale_in == scale_in && scale_speed > 0.0f)
|
||||
{
|
||||
float s = *scale * expf(-scale_in * scale_speed * dt);
|
||||
s = s < scale_lo ? scale_lo : s > scale_hi ? scale_hi : s;
|
||||
if (s == s && s != *scale)
|
||||
{
|
||||
*scale = s;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
// The leash on a table-top board's anchor (god-mode seat): the anchor follows
|
||||
// Lara only once she is further than the radius away. Pure maths.
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
// Pulls anchor toward lara (units, z up) so it stays within radius_units
|
||||
// of her in the horizontal plane and height_units above or below her.
|
||||
// A radius of 0 or less puts the anchor on her horizontally; a height of
|
||||
// 0 or less (the default) likewise vertically. Past the radius the
|
||||
// anchor moves by (lara - anchor) * (d - radius) / d in x and y, so it
|
||||
// ends exactly on the radius with its direction kept.
|
||||
void board_leash(float anchor[3], const float lara[3], float radius_units,
|
||||
float height_units = 0.0f);
|
||||
|
||||
// A stick axis with a dead zone (as Underworld's board stick): 0 inside
|
||||
// |v| < dead, then rescaled so the dead zone's edge is 0 and full tilt
|
||||
// is 1; clamped to -1..1. NaN gives 0.
|
||||
float board_stick_axis(float v, float dead = 0.2f);
|
||||
|
||||
// One frame of the board view's right stick (Underworld's camera_rig
|
||||
// zoom and scale). zoom: the view's distance multiplier (smaller =
|
||||
// closer); scale: the world's shrink factor (Lara 175/scale cm tall).
|
||||
// Inputs -1..1, stick up positive: up zooms in and makes Lara bigger:
|
||||
// zoom *= exp(-zoom_in * zoom_speed * dt), clamped to zoom_lo..hi
|
||||
// scale *= exp(-scale_in * scale_speed * dt), clamped to scale_lo..hi
|
||||
// An axis with no input (or speed <= 0) is left untouched, unclamped.
|
||||
// dt outside (0, 0.5) -- a hitch -- changes nothing. Returns whether
|
||||
// either value changed.
|
||||
bool board_stick_step(float* zoom, float* scale, float zoom_in,
|
||||
float scale_in, float zoom_speed, float scale_speed,
|
||||
float dt, float zoom_lo, float zoom_hi,
|
||||
float scale_lo, float scale_hi);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#include "god_carry.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
Mat4 rotation_about_point(const Mat4& rotation, const float p[3])
|
||||
{
|
||||
return translation(-p[0], -p[1], -p[2]) * rotation * translation(p[0], p[1], p[2]);
|
||||
}
|
||||
|
||||
void bones_transform_range(Mat4* bones, int bone_count, int first, int last,
|
||||
const Mat4& m)
|
||||
{
|
||||
if (!bones)
|
||||
return;
|
||||
if (first < 0) first = 0;
|
||||
if (last >= bone_count) last = bone_count - 1;
|
||||
for (int i = first; i <= last; ++i)
|
||||
bones[i] = bones[i] * m;
|
||||
}
|
||||
|
||||
bool hang_arm_target(const float shoulder[3], const float up[3], float reach,
|
||||
const float toward[3], bool left_arm, bool chain_a,
|
||||
Mat4* target)
|
||||
{
|
||||
if (!target || !(reach > 0.0f))
|
||||
return false;
|
||||
// Horizontal (perpendicular to up) direction toward her head.
|
||||
const float d = toward[0] * up[0] + toward[1] * up[1] + toward[2] * up[2];
|
||||
float p[3] = { toward[0] - up[0] * d, toward[1] - up[1] * d, toward[2] - up[2] * d };
|
||||
const float n = sqrtf(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
|
||||
if (!(n > 1e-6f) || !std::isfinite(n))
|
||||
return false;
|
||||
for (float& c : p) c /= n;
|
||||
const float s = left_arm ? 1.0f : -1.0f; // hand's right axis
|
||||
const float r[3] = { p[0] * s, p[1] * s, p[2] * s };
|
||||
// down = forward(=up) x right, as the camera's rows (right, down,
|
||||
// forward) are related.
|
||||
const float dn[3] = { up[1] * r[2] - up[2] * r[1],
|
||||
up[2] * r[0] - up[0] * r[2],
|
||||
up[0] * r[1] - up[1] * r[0] };
|
||||
const float fx = chain_a ? 1.0f : -1.0f;
|
||||
const float palm = left_arm ? -1.0f : 1.0f;
|
||||
Mat4 m = Mat4::identity();
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
m.m[0][k] = fx * up[k];
|
||||
m.m[1][k] = dn[k];
|
||||
m.m[2][k] = palm * r[k];
|
||||
m.m[3][k] = shoulder[k] + up[k] * reach;
|
||||
}
|
||||
*target = m;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool world_to_head_frame(const Mat4& camera, const Mat4& head_view,
|
||||
float units_per_metre, const float world[3],
|
||||
float out[3])
|
||||
{
|
||||
Mat4 inverse;
|
||||
if (!invert(camera, &inverse) || !(units_per_metre > 0.0f))
|
||||
return false;
|
||||
// world = view * F * camera -> view = world * camera^-1 * F.
|
||||
float v[3];
|
||||
for (int c = 0; c < 3; ++c)
|
||||
v[c] = world[0] * inverse.m[0][c] + world[1] * inverse.m[1][c] +
|
||||
world[2] * inverse.m[2][c] + inverse.m[3][c];
|
||||
v[1] = -v[1];
|
||||
// view = head_view.t + units * (q_game * head_rotation).
|
||||
float q[3];
|
||||
for (int r = 0; r < 3; ++r)
|
||||
{
|
||||
float sum = 0.0f;
|
||||
for (int c = 0; c < 3; ++c)
|
||||
sum += (v[c] - head_view.m[3][c]) * head_view.m[r][c]; // x R^T
|
||||
q[r] = sum / units_per_metre;
|
||||
}
|
||||
out[0] = q[0];
|
||||
out[1] = q[1];
|
||||
out[2] = -q[2];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
// Pure helpers for drawing Lara held in the god hands (docs/GOD_HANDS_PLAN.md
|
||||
// 4.2 / 4.4) and for the test aid that tells where she is in the head frame.
|
||||
// No config(), no game memory, no Windows calls: mathtest links it.
|
||||
|
||||
#include "../vr_math.h"
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
// translate(-p) * R * translate(p) (row vectors): turns things about p.
|
||||
Mat4 rotation_about_point(const Mat4& rotation, const float p[3]);
|
||||
|
||||
// bones[i] = bones[i] * m for i in first..last (clipped to the array).
|
||||
void bones_transform_range(Mat4* bones, int bone_count, int first, int last,
|
||||
const Mat4& m);
|
||||
|
||||
// The procedural hang (4.4): the wrist target that raises one arm
|
||||
// straight up. wrist = shoulder + up * reach (reach is the arm's length
|
||||
// times a little slack, chosen by the caller). Axes as first-person
|
||||
// hands (AUDIT 17.3): x row = the fingers (+up on chain A, -up on B),
|
||||
// y row = down of the hand frame, z row = palm * right, where the hand's
|
||||
// right axis is the horizontal direction toward `toward` (her head) for a
|
||||
// left arm and away from it for a right arm, so the palm faces her head.
|
||||
// `up` is a unit vector; `toward` any non-parallel vector. Returns false
|
||||
// on degenerate input.
|
||||
bool hang_arm_target(const float shoulder[3], const float up[3], float reach,
|
||||
const float toward[3], bool left_arm, bool chain_a,
|
||||
Mat4* target);
|
||||
|
||||
// A game-world point in the head frame the hand script uses (OpenXR
|
||||
// axes in metres: x right, y up, -z forward). camera: the rig's camera
|
||||
// before the head (rows right, down, forward, position, as
|
||||
// god_hand_grip_to_world takes it); head_view: vr_head_to_game_view;
|
||||
// units_per_metre: world_scale * the view's factor. The exact inverse of
|
||||
// placing a hand at that offset from the head. False if the camera does
|
||||
// not invert.
|
||||
bool world_to_head_frame(const Mat4& camera, const Mat4& head_view,
|
||||
float units_per_metre, const float world[3],
|
||||
float out[3]);
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
#include "god_pluck.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void hang_dir(float tx, float ty, float out[3])
|
||||
{
|
||||
out[0] = sinf(tx);
|
||||
out[1] = sinf(ty);
|
||||
const float rest = 1.0f - out[0] * out[0] - out[1] * out[1];
|
||||
out[2] = -sqrtf(rest > 0.05f ? rest : 0.05f);
|
||||
}
|
||||
|
||||
float clampf(float v, float lo, float hi) { return v < lo ? lo : v > hi ? hi : v; }
|
||||
|
||||
float length3(const float v[3]) { return sqrtf(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); }
|
||||
|
||||
void clear_hold(PluckState& s)
|
||||
{
|
||||
s.hand = -1;
|
||||
s.hover = false;
|
||||
s.part = kPluckNone;
|
||||
s.lifted = true;
|
||||
s.flying = false;
|
||||
s.fly_time = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
s.carry[k] = s.arm_offset[k] = s.fly_vel[k] = 0.0f;
|
||||
}
|
||||
|
||||
void clear_pendulum(PluckState& s)
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
s.hand_vel[k] = s.vel_smooth[k] = s.peak_vel[k] = 0.0f;
|
||||
s.peak_age = 0.0f;
|
||||
for (int k = 0; k < 2; ++k)
|
||||
{
|
||||
s.tilt[k] = s.tilt_vel[k] = s.hand_acc[k] = 0.0f;
|
||||
for (auto& limb : s.limb_tilt) limb[k] = 0.0f;
|
||||
}
|
||||
s.body_rotation = Mat4::identity();
|
||||
for (auto& r : s.limb_rotation) r = Mat4::identity();
|
||||
}
|
||||
|
||||
bool is_arm(int part) { return part == kPluckArm0 || part == kPluckArm1; }
|
||||
|
||||
// The wrist she hangs from, if any: the raised arm of an arm grab, or
|
||||
// the caller's wrist pivot for a head grab.
|
||||
bool pivot_of(const PluckState& s, const PluckInput& in, float out[3])
|
||||
{
|
||||
if (is_arm(s.part))
|
||||
{
|
||||
const int c = s.part - kPluckArm0;
|
||||
if (!in.arm_valid[c])
|
||||
return false;
|
||||
for (int k = 0; k < 3; ++k) out[k] = in.arm_pivot[c][k];
|
||||
return true;
|
||||
}
|
||||
if (!in.wrist_pivot_valid)
|
||||
return false;
|
||||
for (int k = 0; k < 3; ++k) out[k] = in.wrist_pivot[k];
|
||||
return true;
|
||||
}
|
||||
|
||||
// The limbs follow the body's tilt on slower pendulums, with a little
|
||||
// idle sway.
|
||||
void swing_limbs(PluckState& s, float dt)
|
||||
{
|
||||
s.phase += dt * 6.2831853f * 1.3f;
|
||||
const float lag[4] = { 0.16f, 0.16f, 0.28f, 0.28f };
|
||||
for (int l = 0; l < 4; ++l)
|
||||
for (int k = 0; k < 2; ++k)
|
||||
{
|
||||
const float follow = dt / (dt + lag[l]);
|
||||
s.limb_tilt[l][k] += (s.tilt[k] - s.limb_tilt[l][k]) * follow;
|
||||
}
|
||||
}
|
||||
|
||||
void update_rotations(PluckState& s)
|
||||
{
|
||||
const float down[3] = { 0.0f, 0.0f, -1.0f };
|
||||
float body[3];
|
||||
hang_dir(s.tilt[0], s.tilt[1], body);
|
||||
s.body_rotation = rotation_between(down, body);
|
||||
for (int l = 0; l < 4; ++l)
|
||||
{
|
||||
const float idle = 0.06f * sinf(s.phase + 1.7f * l);
|
||||
float limb[3];
|
||||
hang_dir(s.limb_tilt[l][0] + idle, s.limb_tilt[l][1] - 0.5f * idle, limb);
|
||||
s.limb_rotation[l] = rotation_between(body, limb);
|
||||
}
|
||||
}
|
||||
|
||||
// The zone `pt` is deepest in (distance / radius), kPluckNone if none.
|
||||
int zone_of(const PluckParams& p, const PluckInput& in, const float pt[3])
|
||||
{
|
||||
int best = kPluckNone;
|
||||
float best_score = 1e30f;
|
||||
if (p.head_only)
|
||||
{
|
||||
if (in.head_valid)
|
||||
{
|
||||
const float d[3] = { pt[0] - in.head[0], pt[1] - in.head[1], pt[2] - in.head[2] };
|
||||
const float r = pluck_head_radius(p);
|
||||
const float dist = length3(d);
|
||||
if (dist <= r)
|
||||
{
|
||||
best = kPluckHead;
|
||||
best_score = r > 0.0f ? dist / r : 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float radius, bottom, top;
|
||||
pluck_zone(p, in.root, &radius, &bottom, &top);
|
||||
const float dx = pt[0] - in.root[0];
|
||||
const float dy = pt[1] - in.root[1];
|
||||
if (dx * dx + dy * dy <= radius * radius && pt[2] >= bottom && pt[2] <= top)
|
||||
{
|
||||
best = kPluckHead; // the column: her whole body, held as before
|
||||
best_score = 1.0f;
|
||||
}
|
||||
}
|
||||
if (p.arm_radius > 0.0f)
|
||||
{
|
||||
const float r = p.arm_radius * p.metre + 0.01f * p.table;
|
||||
for (int c = 0; c < 2; ++c)
|
||||
{
|
||||
if (!in.arm_valid[c])
|
||||
continue;
|
||||
const float dist = pluck_segment_distance(in.arm_shoulder[c], in.arm_end[c], pt);
|
||||
const float score = r > 0.0f ? dist / r : 0.0f;
|
||||
if (dist <= r && score < best_score)
|
||||
{
|
||||
best = kPluckArm0 + c;
|
||||
best_score = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
void pluck_reset(PluckState& s)
|
||||
{
|
||||
clear_hold(s);
|
||||
for (int h = 0; h < 2; ++h)
|
||||
{
|
||||
s.was[h] = s.inside[h] = false;
|
||||
s.inside_age[h] = 1e9f;
|
||||
s.last_zone[h] = kPluckNone;
|
||||
}
|
||||
s.floor_valid = false;
|
||||
s.wrist_pivot = false;
|
||||
clear_pendulum(s);
|
||||
}
|
||||
|
||||
float pluck_head_radius(const PluckParams& p)
|
||||
{
|
||||
return p.head_radius * p.metre + 0.01f * p.table;
|
||||
}
|
||||
|
||||
float pluck_segment_distance(const float a[3], const float b[3], const float p[3])
|
||||
{
|
||||
float ab[3], ap[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
ab[k] = b[k] - a[k];
|
||||
ap[k] = p[k] - a[k];
|
||||
}
|
||||
const float len2 = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
|
||||
float t = len2 > 0.0f ? (ap[0] * ab[0] + ap[1] * ab[1] + ap[2] * ab[2]) / len2 : 0.0f;
|
||||
t = clampf(t, 0.0f, 1.0f);
|
||||
float d2 = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
const float d = ap[k] - ab[k] * t;
|
||||
d2 += d * d;
|
||||
}
|
||||
return sqrtf(d2);
|
||||
}
|
||||
|
||||
bool pluck_near(const PluckParams& p, const float feet[3], const float head_top[3],
|
||||
const float point[3], float reach)
|
||||
{
|
||||
const float r = reach * p.metre + 0.03f * p.table;
|
||||
return pluck_segment_distance(feet, head_top, point) <= r;
|
||||
}
|
||||
|
||||
void pluck_zone(const PluckParams& p, const float root[3],
|
||||
float* radius, float* bottom, float* top)
|
||||
{
|
||||
// The zone is her feet; with a palm platform it would be the carried
|
||||
// root instead (Legend adds the carry while palm >= 0).
|
||||
*radius = 0.30f * p.metre + 0.04f * p.table;
|
||||
*bottom = root[2] - 0.03f * p.table;
|
||||
*top = root[2] + 1.8f * p.metre + 0.04f * p.table;
|
||||
}
|
||||
|
||||
bool pluck_drawn(const PluckState& s)
|
||||
{
|
||||
return (s.hand >= 0 && s.lifted) || s.flying;
|
||||
}
|
||||
|
||||
void pluck_land(PluckState& s)
|
||||
{
|
||||
clear_hold(s);
|
||||
clear_pendulum(s);
|
||||
}
|
||||
|
||||
void pluck_bounce(PluckState& s, const float feet[3], float keep, bool ceiling)
|
||||
{
|
||||
if (!s.flying)
|
||||
return;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.hang[k] += feet[k] - s.target[k];
|
||||
s.target[k] = s.fly_prev[k] = feet[k];
|
||||
s.carry[k] = s.target[k] - s.root[k];
|
||||
}
|
||||
if (ceiling)
|
||||
{
|
||||
if (s.fly_vel[2] > 0.0f)
|
||||
s.fly_vel[2] = -keep * s.fly_vel[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
s.fly_vel[0] = -keep * s.fly_vel[0];
|
||||
s.fly_vel[1] = -keep * s.fly_vel[1];
|
||||
}
|
||||
}
|
||||
|
||||
void pluck_step(PluckState& s, const PluckParams& p, const PluckInput& in,
|
||||
PluckEvents* events)
|
||||
{
|
||||
PluckEvents none;
|
||||
PluckEvents& ev = events ? *events : none;
|
||||
ev = PluckEvents();
|
||||
|
||||
if (!p.enabled)
|
||||
{
|
||||
// Same as leaving board gameplay: no pick-up, and the grips are
|
||||
// remembered so one held now is not a press when it comes back.
|
||||
pluck_reset(s);
|
||||
for (int h = 0; h < 2; ++h) s.was[h] = in.grip[h];
|
||||
return;
|
||||
}
|
||||
for (int k = 0; k < 3; ++k) s.root[k] = in.root[k];
|
||||
const float dt = in.dt;
|
||||
const bool step_ok = dt > 0.0f && dt <= 0.1f && p.table > 0.0f;
|
||||
|
||||
// Thrown: the hang point flies under gravity; nothing holds her
|
||||
// straight, so she keeps turning as she left the hand, slowing a
|
||||
// little. The caller watches fly_prev -> target for what she hits.
|
||||
if (s.flying)
|
||||
{
|
||||
if (step_ok)
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.fly_prev[k] = s.target[k];
|
||||
s.hang[k] += s.fly_vel[k] * dt;
|
||||
}
|
||||
s.fly_vel[2] -= p.throw_gravity * p.table * dt;
|
||||
s.fly_time += dt;
|
||||
for (int k = 0; k < 2; ++k)
|
||||
{
|
||||
s.tilt_vel[k] /= 1.0f + 1.5f * dt;
|
||||
s.tilt[k] = clampf(s.tilt[k] + s.tilt_vel[k] * dt, -1.2f, 1.2f);
|
||||
}
|
||||
swing_limbs(s, dt);
|
||||
}
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.target[k] = s.hang[k] + s.fly_offset[k];
|
||||
s.carry[k] = s.target[k] - in.root[k];
|
||||
}
|
||||
float pivot[3];
|
||||
s.wrist_pivot = pivot_of(s, in, pivot);
|
||||
if (s.wrist_pivot)
|
||||
for (int k = 0; k < 3; ++k) s.pivot[k] = pivot[k];
|
||||
update_rotations(s);
|
||||
if (s.fly_time >= p.throw_max_time)
|
||||
ev.flight_over = true;
|
||||
}
|
||||
|
||||
s.hover = false;
|
||||
for (int h = 0; h < 2; ++h)
|
||||
{
|
||||
const bool pressed = in.grip[h] && !s.was[h];
|
||||
const bool released = !in.grip[h] && s.was[h];
|
||||
s.was[h] = in.grip[h];
|
||||
if (!in.point_valid[h])
|
||||
{
|
||||
s.inside[h] = false;
|
||||
s.inside_age[h] = 1e9f;
|
||||
// A held Lara is not stuck when her hand's point is lost
|
||||
// (Legend would have skipped the hand, release included).
|
||||
if (s.hand != h)
|
||||
continue;
|
||||
}
|
||||
const float* pt = in.point[h];
|
||||
const bool free = s.hand < 0 && !s.flying;
|
||||
if (in.point_valid[h])
|
||||
{
|
||||
int zone = zone_of(p, in, pt);
|
||||
const bool inside = zone != kPluckNone;
|
||||
if (inside && !s.inside[h] && free)
|
||||
ev.hover_enter[h] = true;
|
||||
s.inside[h] = inside;
|
||||
if (inside && free)
|
||||
s.hover = true;
|
||||
if (inside)
|
||||
{
|
||||
s.inside_age[h] = 0.0f;
|
||||
s.last_zone[h] = zone;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.inside_age[h] += step_ok ? dt : 0.0f;
|
||||
if (s.inside_age[h] <= kPluckGraceSeconds)
|
||||
zone = s.last_zone[h];
|
||||
}
|
||||
|
||||
if (free && pressed && zone != kPluckNone && !in.part_held[h])
|
||||
{
|
||||
s.hand = h;
|
||||
s.part = zone;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.grab_offset[k] = in.root[k] - pt[k];
|
||||
s.last_hand[k] = pt[k];
|
||||
}
|
||||
clear_pendulum(s);
|
||||
s.lifted = true;
|
||||
s.settle = p.settle_seconds;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.arm_offset[k] = 0.0f;
|
||||
s.anchor[k] = in.root[k];
|
||||
}
|
||||
if (is_arm(zone))
|
||||
{
|
||||
// Held where it was gripped: her wrist keeps its
|
||||
// offset from the pinch, so nothing jumps.
|
||||
const int c = zone - kPluckArm0;
|
||||
float start[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.arm_offset[k] = in.arm_wrist[c][k] - pt[k];
|
||||
s.lead_target[k] = s.anchor[k] = in.arm_wrist[c][k];
|
||||
start[k] = in.arm_wrist[c][k] - in.arm_shoulder[c][k];
|
||||
}
|
||||
// Her reach, or (an arm hanging nearly straight is
|
||||
// already there) 5 cm at her size past where it was.
|
||||
const float from = length3(start) + 0.05f * p.metre;
|
||||
s.lead_lift = in.arm_reach[c] > from ? in.arm_reach[c] : from;
|
||||
s.lifted = !p.arm_lead;
|
||||
}
|
||||
ev.picked_up = true;
|
||||
ev.hand = h;
|
||||
ev.part = zone;
|
||||
s.hover = false;
|
||||
}
|
||||
}
|
||||
if (s.hand != h)
|
||||
continue;
|
||||
|
||||
if (in.point_valid[h])
|
||||
{
|
||||
if (s.settle > 0.0f)
|
||||
{
|
||||
// Settling: the anchor stays put under the moving pinch.
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
if (is_arm(s.part))
|
||||
s.arm_offset[k] = s.anchor[k] - pt[k];
|
||||
else
|
||||
s.grab_offset[k] = s.anchor[k] - pt[k];
|
||||
s.last_hand[k] = pt[k];
|
||||
}
|
||||
s.settle -= step_ok ? dt : 0.0f;
|
||||
}
|
||||
for (int k = 0; k < 3; ++k) s.hang[k] = pt[k] + s.arm_offset[k];
|
||||
float pivot[3];
|
||||
s.wrist_pivot = pivot_of(s, in, pivot);
|
||||
if (s.wrist_pivot)
|
||||
for (int k = 0; k < 3; ++k) s.pivot[k] = pivot[k];
|
||||
|
||||
// Arm lead: only her arm follows until the hand pulls past
|
||||
// its reach; then she leaves the ground hanging from that
|
||||
// wrist, her body starting along the arm's line (shoulder
|
||||
// and wrist stay put) and swinging down under the hand.
|
||||
if (!s.lifted)
|
||||
{
|
||||
for (int k = 0; k < 3; ++k) s.lead_target[k] = s.hang[k];
|
||||
const int c = s.part - kPluckArm0;
|
||||
if (in.arm_valid[c] && in.arm_reach[c] > 0.0f)
|
||||
{
|
||||
float d[3];
|
||||
for (int k = 0; k < 3; ++k) d[k] = s.hang[k] - in.arm_shoulder[c][k];
|
||||
const float len = length3(d);
|
||||
if (len > s.lead_lift)
|
||||
{
|
||||
s.lifted = true;
|
||||
ev.lifted = true;
|
||||
ev.hand = h;
|
||||
for (int k = 0; k < 2; ++k)
|
||||
{
|
||||
s.tilt[k] = asinf(clampf(-d[k] / len, -0.93f, 0.93f));
|
||||
s.tilt_vel[k] = 0.0f;
|
||||
for (auto& limb : s.limb_tilt) limb[k] = s.tilt[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
if (!s.lifted)
|
||||
s.target[k] = in.root[k];
|
||||
else
|
||||
s.target[k] = s.hang[k] + (s.wrist_pivot ? in.root[k] - pivot[k]
|
||||
: s.grab_offset[k]);
|
||||
s.carry[k] = s.target[k] - in.root[k];
|
||||
}
|
||||
|
||||
// Dangle: a pendulum of a doll-sized length hanging from the
|
||||
// grab point, driven by the hand's horizontal acceleration
|
||||
// in real metres; the limbs follow on slower, lagging
|
||||
// pendulums with a little idle sway.
|
||||
if (step_ok)
|
||||
{
|
||||
const float to_metres = 1.0f / p.table;
|
||||
const float smooth_v = dt / (dt + 0.03f);
|
||||
float step_v[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
step_v[k] = (pt[k] - s.last_hand[k]) / dt * to_metres;
|
||||
// A tracking jump (or a script ending) is no throw.
|
||||
const bool jump = length3(step_v) > 4.0f * p.throw_max_speed;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
const float v = jump ? s.hand_vel[k] : step_v[k];
|
||||
if (k < 2)
|
||||
{
|
||||
const float a = (v - s.hand_vel[k]) / dt;
|
||||
const float smooth = dt / (dt + 0.03f);
|
||||
s.hand_acc[k] += (a - s.hand_acc[k]) * smooth;
|
||||
}
|
||||
s.hand_vel[k] = v;
|
||||
s.last_hand[k] = pt[k];
|
||||
s.vel_smooth[k] += (v - s.vel_smooth[k]) * smooth_v;
|
||||
}
|
||||
// A throw takes the fastest of the last 0.15 s: the
|
||||
// hand slows while the fingers open.
|
||||
s.peak_age += dt;
|
||||
if (length3(s.vel_smooth) >= length3(s.peak_vel) || s.peak_age > 0.15f)
|
||||
{
|
||||
for (int k = 0; k < 3; ++k) s.peak_vel[k] = s.vel_smooth[k];
|
||||
s.peak_age = 0.0f;
|
||||
}
|
||||
if (s.lifted)
|
||||
{
|
||||
const float length = 0.09f; // metres, doll scale
|
||||
const float gravity = 9.81f;
|
||||
for (int k = 0; k < 2; ++k)
|
||||
{
|
||||
const float accel = -gravity / length *
|
||||
sinf(s.tilt[k]) - 3.0f * s.tilt_vel[k] -
|
||||
s.hand_acc[k] / length;
|
||||
s.tilt_vel[k] += accel * dt;
|
||||
s.tilt[k] = clampf(s.tilt[k] + s.tilt_vel[k] * dt, -1.2f, 1.2f);
|
||||
}
|
||||
swing_limbs(s, dt);
|
||||
}
|
||||
}
|
||||
update_rotations(s);
|
||||
}
|
||||
|
||||
// The floor under the held Lara, probed by the caller from a
|
||||
// little above her feet (she may be pushed slightly into a
|
||||
// surface): it is what a release lands on.
|
||||
s.floor_valid = in.floor_valid;
|
||||
if (in.floor_valid)
|
||||
for (int k = 0; k < 3; ++k) s.floor[k] = in.floor[k];
|
||||
|
||||
if (!released)
|
||||
continue;
|
||||
ev.released = true;
|
||||
ev.hand = h;
|
||||
ev.part = s.part;
|
||||
ev.dropped_to_floor = s.floor_valid;
|
||||
for (int k = 0; k < 3; ++k) ev.target[k] = s.target[k];
|
||||
if (!s.lifted)
|
||||
{
|
||||
// Her arm let go before she left the ground.
|
||||
ev.moved = false;
|
||||
clear_hold(s);
|
||||
clear_pendulum(s);
|
||||
continue;
|
||||
}
|
||||
float speed = length3(s.peak_vel);
|
||||
if (p.throw_enabled && speed >= p.throw_min_speed)
|
||||
{
|
||||
const float cap = speed > p.throw_max_speed ? p.throw_max_speed / speed : 1.0f;
|
||||
speed *= cap;
|
||||
ev.thrown = true;
|
||||
ev.throw_speed = speed;
|
||||
s.flying = true;
|
||||
s.fly_time = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.fly_vel[k] = s.peak_vel[k] * cap * p.throw_scale * p.table;
|
||||
s.fly_offset[k] = s.target[k] - s.hang[k];
|
||||
s.fly_prev[k] = s.target[k];
|
||||
}
|
||||
s.hand = -1;
|
||||
s.hover = false;
|
||||
continue;
|
||||
}
|
||||
clear_hold(s);
|
||||
clear_pendulum(s);
|
||||
}
|
||||
}
|
||||
|
||||
Mat4 pluck_body_matrix(const PluckState& s, const PluckInput& in)
|
||||
{
|
||||
float grab[3];
|
||||
if (!pivot_of(s, in, grab))
|
||||
for (int k = 0; k < 3; ++k)
|
||||
grab[k] = in.root[k] - s.grab_offset[k];
|
||||
return translation(-grab[0], -grab[1], -grab[2]) * s.body_rotation *
|
||||
translation(s.hang[0], s.hang[1], s.hang[2]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
#pragma once
|
||||
// Picking Lara up with a pinch and holding her by the feet (ported back from
|
||||
// the Underworld mod, which took it from Legend's board pick-up). Pure maths
|
||||
// -- no config(), no game memory, no Windows calls, no clocks -- so mathtest
|
||||
// links it. The caller feeds one PluckInput per frame and reads the state
|
||||
// back.
|
||||
//
|
||||
// Game space is z-up. A hand grips inside a zone around her feet to pick her
|
||||
// up; she then hangs from the hand's pinch point as a pendulum, her limbs
|
||||
// lagging behind on slower pendulums with a little idle sway. The drawn body
|
||||
// is translate(-grab) * body_rotation * translate(hang) (row vectors), the
|
||||
// arms (limb 0/1, at the shoulders) and legs (2/3, at the hips) turned about
|
||||
// their joints by limb_rotation[l] first.
|
||||
//
|
||||
// Left out on purpose (Legend has it, docs/GOD_HANDS_PLAN.md phase 5): the
|
||||
// palm platform and hand-to-hand handover. The zone is computed by one
|
||||
// function (pluck_zone) and the held/not-held split is in one place, so a
|
||||
// `palm` field can be added later without reshaping the API.
|
||||
|
||||
#include "../vr_math.h"
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
struct PluckParams
|
||||
{
|
||||
float metre = 1.0f; // game units per real metre
|
||||
float table = 1.0f; // game units per table metre (metre * board factor)
|
||||
bool enabled = true; // false: behaves like pluck_reset
|
||||
// Underworld (user 2026-10-16): she is picked up only by her head
|
||||
// (or near it): the zone is a ball round PluckInput::head instead of
|
||||
// Legend's column round her whole body, so a grip on her chest holds
|
||||
// the chest, and grips elsewhere stay the game's.
|
||||
bool head_only = false;
|
||||
float head_radius = 0.10f; // metres at her scale ([camera] god_head_zone)
|
||||
// Arm zones (user 2026-10-19): a capsule along each arm, shoulder to
|
||||
// fingertips (PluckInput::arm_*), this radius at her scale plus 1 cm of
|
||||
// the player's hand; 0 = no arm grabs ([camera] god_arm_zone). The
|
||||
// zone the grip is deepest in (distance / radius) wins.
|
||||
float arm_radius = 0.0f;
|
||||
// An arm grab first moves only her arm (the caller bends it toward
|
||||
// PluckState::lead_target) and lifts her once the hand pulls past the
|
||||
// arm's reach; off, she is lifted at once ([camera] god_arm_lead).
|
||||
bool arm_lead = true;
|
||||
// Throwing ([camera] god_throw*): let go while the hand moves faster
|
||||
// than throw_min_speed (real m/s, the peak of the last 0.15 s) and she
|
||||
// flies from the hand at that velocity times throw_scale, under
|
||||
// throw_gravity (real m/s^2: she is a doll on the player's table).
|
||||
// After a grab, the pinch point still moves for a moment (the fingers
|
||||
// finish closing; the held hand takes the pinch pose): for this long
|
||||
// the hold re-anchors to it, so she does not follow that motion.
|
||||
float settle_seconds = 0.0f;
|
||||
bool throw_enabled = false;
|
||||
float throw_min_speed = 0.6f;
|
||||
float throw_scale = 1.0f;
|
||||
float throw_gravity = 9.81f;
|
||||
// A throw is no faster than this (real m/s, before throw_scale);
|
||||
// hand samples over 4x it are tracking jumps and ignored.
|
||||
float throw_max_speed = 6.0f;
|
||||
float throw_max_time = 3.0f; // seconds; then events.flight_over
|
||||
};
|
||||
|
||||
// Which part of her a grip holds (PluckState::part).
|
||||
enum : int { kPluckNone = -1, kPluckHead = 0, kPluckArm0 = 1, kPluckArm1 = 2 };
|
||||
constexpr float kPluckGraceSeconds = 0.25f;
|
||||
|
||||
struct PluckInput
|
||||
{
|
||||
float dt = 0.0f; // seconds; outside (0, 0.1] no physics this frame
|
||||
bool grip[2] = {}; // hand grip pressed (0 left, 1 right)
|
||||
bool point_valid[2] = {};
|
||||
float point[2][3] = {}; // each hand's pinch point (or index tip), game space
|
||||
float root[3] = {}; // Lara's real feet
|
||||
bool head_valid = false; // head_only: her head (between head base and top)
|
||||
float head[3] = {};
|
||||
bool part_held[2] = {}; // that hand holds one of her soft parts: no pick-up
|
||||
bool wrist_pivot_valid = false; // a dangle animation hangs her by a wrist
|
||||
float wrist_pivot[3] = {}; // that wrist's position
|
||||
bool floor_valid = false; // probe straight down under the held target
|
||||
float floor[3] = {};
|
||||
// Her arms (0/1, the caller's arm chains): the grab capsule from the
|
||||
// shoulder to the fingertips, the real wrist, the arm's reach
|
||||
// (shoulder to wrist, stretched), and the wrist of that arm raised
|
||||
// straight up (the hang pivot of an arm grab).
|
||||
bool arm_valid[2] = {};
|
||||
float arm_shoulder[2][3] = {};
|
||||
float arm_end[2][3] = {};
|
||||
float arm_wrist[2][3] = {};
|
||||
float arm_reach[2] = {};
|
||||
float arm_pivot[2][3] = {};
|
||||
};
|
||||
|
||||
struct PluckEvents
|
||||
{
|
||||
bool picked_up = false;
|
||||
bool released = false;
|
||||
int hand = -1; // the hand of either event
|
||||
bool dropped_to_floor = false; // released with a floor below
|
||||
float target[3] = {}; // where her feet were held (release)
|
||||
bool hover_enter[2] = {}; // a free hand just entered the zone
|
||||
int part = kPluckNone; // picked_up: what the grip holds
|
||||
bool lifted = false; // an arm lead just lifted her off her feet
|
||||
bool moved = true; // released: false = she was never lifted (arm lead)
|
||||
bool thrown = false; // released into a flight (PluckState::flying)
|
||||
float throw_speed = 0.0f; // thrown: the hand's speed, real m/s
|
||||
bool flight_over = false; // flying for throw_max_time: put her down
|
||||
};
|
||||
|
||||
struct PluckState
|
||||
{
|
||||
int hand = -1; // hand holding Lara, -1 none
|
||||
bool was[2] = {}; // grip last frame
|
||||
bool inside[2] = {}; // hand in the zone last frame
|
||||
// Seconds since each hand was last in a zone, and which: a grip
|
||||
// closing pulls the pinch point (thumb and index tips) back into the
|
||||
// palm, out of a small zone, before it reads as a grab, so a press
|
||||
// within kPluckGraceSeconds still takes that zone.
|
||||
float inside_age[2] = { 1e9f, 1e9f };
|
||||
int last_zone[2] = { kPluckNone, kPluckNone };
|
||||
bool hover = false; // a free hand is in the zone
|
||||
float root[3] = {}; // her real feet, last step
|
||||
float grab_offset[3] = {}; // root - pinch point at the grab
|
||||
float carry[3] = {}; // drawn offset while held (target - root)
|
||||
float target[3] = {}; // where her feet are held
|
||||
float hang[3] = {}; // pinch point of the holding hand
|
||||
bool floor_valid = false;
|
||||
float floor[3] = {};
|
||||
// Pendulum, real metres: body tilt angles about world x/y, and the
|
||||
// lagging limb tilts (0/1 arms, 2/3 legs).
|
||||
float last_hand[3] = {};
|
||||
float hand_vel[3] = {};
|
||||
float hand_acc[2] = {};
|
||||
float tilt[2] = {};
|
||||
float tilt_vel[2] = {};
|
||||
float limb_tilt[4][2] = {};
|
||||
float phase = 0.0f;
|
||||
bool wrist_pivot = false; // hung by a wrist this frame
|
||||
float pivot[3] = {};
|
||||
int part = kPluckNone; // what the holding grip holds (kept in flight)
|
||||
bool lifted = true; // false: an arm lead, her body not moved yet
|
||||
float arm_offset[3] = {}; // arm grab: her wrist - pinch point at the grab
|
||||
float lead_target[3] = {}; // arm lead: where her wrist is drawn
|
||||
float lead_lift = 0.0f; // arm lead: shoulder-to-wrist distance that lifts her
|
||||
float settle = 0.0f; // seconds of re-anchoring left (PluckParams::settle_seconds)
|
||||
float anchor[3] = {}; // what the hold keeps still while settling: her
|
||||
// root (head grab) or wrist (arm grab) at the grab
|
||||
// The hand's velocity (real m/s): smoothed, and its peak over the
|
||||
// last 0.15 s (what a throw takes).
|
||||
float vel_smooth[3] = {};
|
||||
float peak_vel[3] = {};
|
||||
float peak_age = 0.0f;
|
||||
// Thrown: the hang point flies (game units/s), her feet stay
|
||||
// fly_offset from it; fly_prev = her feet last step (the caller
|
||||
// probes fly_prev -> target for what she hits).
|
||||
bool flying = false;
|
||||
float fly_vel[3] = {};
|
||||
float fly_offset[3] = {};
|
||||
float fly_prev[3] = {};
|
||||
float fly_time = 0.0f;
|
||||
Mat4 body_rotation = Mat4::identity();
|
||||
Mat4 limb_rotation[4] = { Mat4::identity(), Mat4::identity(),
|
||||
Mat4::identity(), Mat4::identity() };
|
||||
};
|
||||
|
||||
// Drops a held Lara where she was (leaving board gameplay) and clears
|
||||
// everything, the grips seen so far included.
|
||||
void pluck_reset(PluckState& s);
|
||||
|
||||
// Pinch zone around her feet: horizontal radius, and the z range.
|
||||
// (head_only: the head ball's radius is pluck_head_radius.)
|
||||
void pluck_zone(const PluckParams& p, const float root[3],
|
||||
float* radius, float* bottom, float* top);
|
||||
|
||||
// Head ball: head_radius at her scale plus 1 cm of the player's own
|
||||
// hand. (A grip on a soft part still wins: PluckInput::part_held.)
|
||||
float pluck_head_radius(const PluckParams& p);
|
||||
|
||||
// Near her (the god hands' input claim, user 2026-10-16: a larger zone
|
||||
// than touching): within `reach` metres at her scale, plus 3 cm of the
|
||||
// player's hand, of the segment from her feet to the top of her head.
|
||||
bool pluck_near(const PluckParams& p, const float feet[3], const float head_top[3],
|
||||
const float point[3], float reach);
|
||||
|
||||
// Distance from p to the segment a-b.
|
||||
float pluck_segment_distance(const float a[3], const float b[3], const float p[3]);
|
||||
|
||||
// One frame. `events` may be null.
|
||||
void pluck_step(PluckState& s, const PluckParams& p, const PluckInput& in,
|
||||
PluckEvents* events);
|
||||
|
||||
// She is drawn away from where she really is: held and lifted, or flying.
|
||||
bool pluck_drawn(const PluckState& s);
|
||||
|
||||
// Ends a flight (the caller has put her down where she landed).
|
||||
void pluck_land(PluckState& s);
|
||||
|
||||
// A flight hit something that is not a floor: her feet are moved to
|
||||
// `feet` (the caller's last free point) and she bounces off, keeping
|
||||
// `keep` of her speed: back horizontally off a wall, or (ceiling) her
|
||||
// rise turned into a fall.
|
||||
void pluck_bounce(PluckState& s, const float feet[3], float keep, bool ceiling);
|
||||
|
||||
// translate(-grab) * body_rotation * translate(hang): maps the grab point
|
||||
// (her root - grab_offset, or the wrist pivot: in.wrist_pivot for a head
|
||||
// grab, in.arm_pivot for an arm grab) onto the hang point. Uses the root
|
||||
// and pivots of `in` (the current frame).
|
||||
Mat4 pluck_body_matrix(const PluckState& s, const PluckInput& in);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "menu_panel.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
bool menu_panel_place(const float head[3], float forward_x, float forward_z,
|
||||
float distance, float yaw_left_degrees,
|
||||
float pitch_up_degrees, MenuPanelPose* out)
|
||||
{
|
||||
const float length = sqrtf(forward_x * forward_x + forward_z * forward_z);
|
||||
if (!out || !head || !(length > 1.0e-4f))
|
||||
return false;
|
||||
const float fx = forward_x / length, fz = forward_z / length;
|
||||
// Turned about +y (up): in these right-handed axes a positive turn
|
||||
// takes straight ahead (-z) to the left (-x).
|
||||
const float yaw = yaw_left_degrees * 0.01745329f;
|
||||
const float pitch = pitch_up_degrees * 0.01745329f;
|
||||
const float sy = sinf(yaw), cy = cosf(yaw);
|
||||
const float gx = fx * cy + fz * sy;
|
||||
const float gz = -fx * sy + fz * cy;
|
||||
const float sp = sinf(pitch), cp = cosf(pitch);
|
||||
const float d[3] = { gx * cp, sp, gz * cp };
|
||||
for (int i = 0; i < 3; ++i)
|
||||
out->centre[i] = head[i] + d[i] * distance;
|
||||
// Width level, to the right of the turned direction; height the
|
||||
// third axis (right x direction), leaning back toward the head when
|
||||
// the panel is above it.
|
||||
out->right[0] = -gz;
|
||||
out->right[1] = 0.0f;
|
||||
out->right[2] = gx;
|
||||
out->up[0] = -gx * sp;
|
||||
out->up[1] = cp;
|
||||
out->up[2] = -gz * sp;
|
||||
return true;
|
||||
}
|
||||
|
||||
void menu_panel_corners(const MenuPanelPose& pose, float half_size,
|
||||
float out[4][3])
|
||||
{
|
||||
const float sx[4] = { -1.0f, 1.0f, -1.0f, 1.0f };
|
||||
const float sy[4] = { 1.0f, 1.0f, -1.0f, -1.0f };
|
||||
for (int k = 0; k < 4; ++k)
|
||||
for (int i = 0; i < 3; ++i)
|
||||
out[k][i] = pose.centre[i] +
|
||||
(sx[k] * pose.right[i] + sy[k] * pose.up[i]) * half_size;
|
||||
}
|
||||
|
||||
float menu_panel_half_size(float distance, float width_degrees)
|
||||
{
|
||||
return distance * tanf(width_degrees * 0.5f * 0.01745329f);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
enum { HoldIdle = 0, HoldPending = 1, HoldConsumed = 2 };
|
||||
}
|
||||
|
||||
MenuHoldEvent menu_hold_update(MenuHold& hold, bool down,
|
||||
unsigned long long now_ms,
|
||||
float hold_seconds, bool overlay_busy,
|
||||
bool* to_game)
|
||||
{
|
||||
bool pass = false;
|
||||
MenuHoldEvent event = MenuHoldNone;
|
||||
if (overlay_busy)
|
||||
{
|
||||
// The overlay's button now; a press still down when it closes
|
||||
// is not a new one for the game.
|
||||
hold.state = down ? HoldConsumed : HoldIdle;
|
||||
}
|
||||
else if (!(hold_seconds > 0.0f))
|
||||
{
|
||||
// Off: the button goes straight to the game, except the rest of
|
||||
// a press that opened the overlay.
|
||||
if (hold.state == HoldConsumed && down)
|
||||
pass = false;
|
||||
else
|
||||
{
|
||||
hold.state = HoldIdle;
|
||||
pass = down;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (hold.state)
|
||||
{
|
||||
case HoldIdle:
|
||||
if (down)
|
||||
{
|
||||
hold.state = HoldPending;
|
||||
hold.down_ms = now_ms;
|
||||
}
|
||||
break;
|
||||
case HoldPending:
|
||||
if (!down)
|
||||
{
|
||||
hold.state = HoldIdle;
|
||||
event = MenuHoldTap;
|
||||
}
|
||||
else if (now_ms - hold.down_ms >=
|
||||
(unsigned long long)(hold_seconds * 1000.0f + 0.5f))
|
||||
{
|
||||
hold.state = HoldConsumed;
|
||||
event = MenuHoldOpen;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!down)
|
||||
hold.state = HoldIdle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (to_game)
|
||||
*to_game = pass;
|
||||
return event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
// The settings overlay in the headset (ported from the Underworld mod):
|
||||
// where its panel goes in the room, and the menu button's hold-to-open.
|
||||
// Pure logic, tested in mathtest; vr_submit.cpp draws the panel and
|
||||
// vr_input.cpp feeds the button.
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
// A square panel in OpenVR tracking space (metres; x right, y up, z
|
||||
// back, so straight ahead is -z): its centre and the unit axes along its
|
||||
// width (level) and height. It faces the head it was placed from.
|
||||
struct MenuPanelPose
|
||||
{
|
||||
float centre[3];
|
||||
float right[3];
|
||||
float up[3];
|
||||
};
|
||||
|
||||
// Placed from the head at head[3], whose level forward is (forward_x,
|
||||
// forward_z) (need not be normalised; false if it has no length): the
|
||||
// level direction straight ahead, turned yaw_left_degrees to the left
|
||||
// and pitch_up_degrees up, distance metres out.
|
||||
bool menu_panel_place(const float head[3], float forward_x, float forward_z,
|
||||
float distance, float yaw_left_degrees,
|
||||
float pitch_up_degrees, MenuPanelPose* out);
|
||||
|
||||
// Its corners at half_size metres from the centre each way, in triangle
|
||||
// strip order: top left, top right, bottom left, bottom right (texture
|
||||
// u, v = 0,0 / 1,0 / 0,1 / 1,1).
|
||||
void menu_panel_corners(const MenuPanelPose& pose, float half_size,
|
||||
float out[4][3]);
|
||||
|
||||
// Half its width for a panel width_degrees wide at distance metres.
|
||||
float menu_panel_half_size(float distance, float width_degrees);
|
||||
|
||||
// The menu (pause) button with hold-to-open. With the overlay closed
|
||||
// and hold_seconds > 0 the button does not reach the game while it is
|
||||
// down: let go before hold_seconds it is a tap (the caller pulses
|
||||
// pause), held to hold_seconds it opens the overlay and that press never
|
||||
// reaches the game. While the overlay is open, or its input is held
|
||||
// back after closing (`overlay_busy`), the button is the overlay's.
|
||||
enum MenuHoldEvent
|
||||
{
|
||||
MenuHoldNone,
|
||||
MenuHoldTap, // pulse the game's pause
|
||||
MenuHoldOpen, // open the overlay
|
||||
};
|
||||
|
||||
struct MenuHold
|
||||
{
|
||||
int state = 0; // idle, pending, consumed
|
||||
unsigned long long down_ms = 0;
|
||||
};
|
||||
|
||||
// Once a frame. *to_game: the button as the game should see it held
|
||||
// this frame (only with hold-to-open off).
|
||||
MenuHoldEvent menu_hold_update(MenuHold& hold, bool down,
|
||||
unsigned long long now_ms,
|
||||
float hold_seconds, bool overlay_busy,
|
||||
bool* to_game);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "menu_watermark.h"
|
||||
|
||||
#include "vr_backend.h"
|
||||
#include "../common/log.h"
|
||||
#include "../common/version.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
// "TRL VR 1.1.0 (OpenXR)" on the main menu (user 2026-10-27, after
|
||||
// TR7-Menu-Hook's own watermark). The game's font queues text and draws it at
|
||||
// its next Font::Flush, so printing here -- at present, after this frame's
|
||||
// flush -- shows it from the next frame on, without hooking Font::Flush
|
||||
// (TR7-Menu-Hook hooks that). Addresses are the retail build's (TR7-Menu-Hook
|
||||
// uses the same; PrintFormatted / SetScale confirmed by our symbol remap,
|
||||
// SetCursor by its code: it stores x, y at 0x0107F68C / 0x0107F690).
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const uintptr_t kMainState = 0x010E5868; // GameTracker main state
|
||||
const int kMainStateMainMenu = 6; // MS_DISPLAY_MAIN_MENU
|
||||
const uintptr_t kMainFont = 0x0107F680; // Font* (main font)
|
||||
const uintptr_t kFontSetCursor = 0x00431670; // static (float x, float y)
|
||||
const uintptr_t kFontPrintFormatted = 0x004323D0; // thiscall (text, backdrop)
|
||||
|
||||
using PFN_SetCursor = void(__cdecl*)(float, float);
|
||||
using PFN_PrintFormatted = void(__thiscall*)(void*, const char*, int);
|
||||
|
||||
bool g_failed = false;
|
||||
}
|
||||
|
||||
void menu_watermark_frame()
|
||||
{
|
||||
if (g_failed)
|
||||
return;
|
||||
__try
|
||||
{
|
||||
if (*reinterpret_cast<const int*>(kMainState) != kMainStateMainMenu)
|
||||
return;
|
||||
void* font = *reinterpret_cast<void**>(kMainFont);
|
||||
if (!font)
|
||||
return;
|
||||
const VrBackend backend = vr_backend();
|
||||
const char* text = backend == VrBackendOpenXr
|
||||
? "TRL VR " TRLVR_VERSION " (OpenXR)"
|
||||
: backend == VrBackendOpenVr
|
||||
? "TRL VR " TRLVR_VERSION " (OpenVR)"
|
||||
: "TRL VR " TRLVR_VERSION " (no headset)";
|
||||
// TR7-Menu-Hook prints at (5, 430) in the 640x480 layout; this
|
||||
// goes on the line above it.
|
||||
reinterpret_cast<PFN_SetCursor>(kFontSetCursor)(5.0f, 414.0f);
|
||||
reinterpret_cast<PFN_PrintFormatted>(kFontPrintFormatted)(font, text, 0);
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
g_failed = true;
|
||||
log("menu: version watermark faulted; left off for this session");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
// Once a frame on the game thread: on the main menu, the mod's version
|
||||
// and VR runtime in the corner, in the game's own font.
|
||||
void menu_watermark_frame();
|
||||
}
|
||||
@@ -0,0 +1,693 @@
|
||||
#include "vr_backend.h"
|
||||
|
||||
#include "../common/log.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#define VK_USE_PLATFORM_WIN32_KHR
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <openvr.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// OpenVR / SteamVR behind xr_runtime.h (1.1.0): the fallback backend, built
|
||||
// from the 1.0.0 code that shipped (vr_session.cpp init, vr_submit.cpp
|
||||
// submit). vr_input.cpp decides game actions from physical controls, so
|
||||
// SteamVR's action manifest here is one action per physical control and
|
||||
// hand (trlvr_actions.json + bindings_*.json beside the game); players can
|
||||
// still rebind them in SteamVR's bindings screen.
|
||||
//
|
||||
// openvr_api.dll is loaded at run time, beside the game first.
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace
|
||||
{
|
||||
typedef uint32_t(__cdecl* PFN_InitInternal2)(vr::EVRInitError*, vr::EVRApplicationType, const char*);
|
||||
typedef void(__cdecl* PFN_ShutdownInternal)();
|
||||
typedef void* (__cdecl* PFN_GetGenericInterface)(const char*, vr::EVRInitError*);
|
||||
typedef bool(__cdecl* PFN_IsHmdPresent)();
|
||||
typedef const char* (__cdecl* PFN_GetInitErrorAsEnglishDescription)(vr::EVRInitError);
|
||||
|
||||
HMODULE g_dll = nullptr;
|
||||
PFN_InitInternal2 p_InitInternal2 = nullptr;
|
||||
PFN_ShutdownInternal p_ShutdownInternal = nullptr;
|
||||
PFN_GetGenericInterface p_GetGenericInterface = nullptr;
|
||||
PFN_IsHmdPresent p_IsHmdPresent = nullptr;
|
||||
PFN_GetInitErrorAsEnglishDescription p_ErrorText = nullptr;
|
||||
|
||||
vr::IVRSystem* g_system = nullptr;
|
||||
vr::IVRCompositor* g_comp = nullptr;
|
||||
vr::IVRInput* g_input = nullptr;
|
||||
bool g_initialised = false; // VR_InitInternal2 succeeded
|
||||
bool g_session = false;
|
||||
bool g_frame_begun = false;
|
||||
bool g_input_active = false;
|
||||
double g_wait_ms = 0.0;
|
||||
char g_error[512] = "";
|
||||
char g_system_name[256] = "";
|
||||
|
||||
// One action per physical control and hand.
|
||||
vr::VRActionSetHandle_t g_set = vr::k_ulInvalidActionSetHandle;
|
||||
vr::VRActionHandle_t g_stick[2]{}, g_trigger[2]{}, g_squeeze[2]{};
|
||||
vr::VRActionHandle_t g_button[XrButtonCount][2]{};
|
||||
vr::VRActionHandle_t g_pose[2]{}, g_haptic[2]{};
|
||||
// SteamVR's OpenXR grip pose ("openxr_grip"), read only to log how
|
||||
// far it is from the OpenVR grip pose the hands use (user
|
||||
// 2026-10-27: OpenXR's hands sat a little forward and higher).
|
||||
vr::VRActionHandle_t g_pose_xr[2]{};
|
||||
ULONGLONG g_pose_compare_at = 0;
|
||||
int g_pose_compare_reports = 0;
|
||||
vr::TrackedDevicePose_t g_device_poses[vr::k_unMaxTrackedDeviceCount]{};
|
||||
|
||||
XrControllerKind g_controller[2] = { XrControllerNone, XrControllerNone };
|
||||
char g_profile[2][96] = { "", "" };
|
||||
char g_type[2][64] = { "", "" };
|
||||
ULONGLONG g_profile_checked = 0;
|
||||
|
||||
void set_error(const char* fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(g_error, sizeof(g_error), fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
bool load_dll()
|
||||
{
|
||||
if (g_dll)
|
||||
return true;
|
||||
wchar_t path[MAX_PATH]{};
|
||||
swprintf_s(path, L"%sopenvr_api.dll", exe_dir());
|
||||
g_dll = LoadLibraryExW(path, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
|
||||
if (!g_dll)
|
||||
g_dll = LoadLibraryW(L"openvr_api.dll");
|
||||
if (!g_dll)
|
||||
{
|
||||
set_error("openvr_api.dll not found beside the game or on the path");
|
||||
return false;
|
||||
}
|
||||
p_InitInternal2 = (PFN_InitInternal2)GetProcAddress(g_dll, "VR_InitInternal2");
|
||||
p_ShutdownInternal = (PFN_ShutdownInternal)GetProcAddress(g_dll, "VR_ShutdownInternal");
|
||||
p_GetGenericInterface = (PFN_GetGenericInterface)GetProcAddress(g_dll, "VR_GetGenericInterface");
|
||||
p_IsHmdPresent = (PFN_IsHmdPresent)GetProcAddress(g_dll, "VR_IsHmdPresent");
|
||||
p_ErrorText = (PFN_GetInitErrorAsEnglishDescription)GetProcAddress(g_dll, "VR_GetVRInitErrorAsEnglishDescription");
|
||||
if (!p_InitInternal2 || !p_GetGenericInterface || !p_ShutdownInternal)
|
||||
{
|
||||
set_error("openvr_api.dll is missing the entry points we need");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void* get_interface(const char* version)
|
||||
{
|
||||
vr::EVRInitError err = vr::VRInitError_None;
|
||||
void* p = p_GetGenericInterface(version, &err);
|
||||
return err == vr::VRInitError_None ? p : nullptr;
|
||||
}
|
||||
|
||||
// DXVK reads the Vulkan extensions the runtime needs from the
|
||||
// environment before it creates its instance (dxvk_openxr.cpp, the
|
||||
// same variables the OpenXR backend fills). OpenVR names the device
|
||||
// extensions only for a physical device, so a throwaway instance is
|
||||
// made to ask which one the headset is on.
|
||||
bool publish_vulkan_extensions()
|
||||
{
|
||||
char instance_exts[4096]{};
|
||||
g_comp->GetVulkanInstanceExtensionsRequired(instance_exts,
|
||||
sizeof(instance_exts));
|
||||
|
||||
std::string device_exts;
|
||||
HMODULE vk = LoadLibraryW(L"vulkan-1.dll");
|
||||
auto get_proc = vk ? (PFN_vkGetInstanceProcAddr)GetProcAddress(
|
||||
vk, "vkGetInstanceProcAddr")
|
||||
: nullptr;
|
||||
auto create = get_proc ? (PFN_vkCreateInstance)get_proc(
|
||||
nullptr, "vkCreateInstance")
|
||||
: nullptr;
|
||||
if (create)
|
||||
{
|
||||
std::string list = instance_exts;
|
||||
std::vector<std::string> names;
|
||||
for (size_t a = 0; a < list.size();)
|
||||
{
|
||||
size_t b = list.find(' ', a);
|
||||
if (b == std::string::npos) b = list.size();
|
||||
if (b > a) names.push_back(list.substr(a, b - a));
|
||||
a = b + 1;
|
||||
}
|
||||
std::vector<const char*> ptrs;
|
||||
for (const std::string& n : names)
|
||||
ptrs.push_back(n.c_str());
|
||||
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
||||
app.apiVersion = VK_API_VERSION_1_1;
|
||||
VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
|
||||
info.pApplicationInfo = &app;
|
||||
info.enabledExtensionCount = (uint32_t)ptrs.size();
|
||||
info.ppEnabledExtensionNames = ptrs.data();
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
if (create(&info, nullptr, &instance) == VK_SUCCESS)
|
||||
{
|
||||
uint64_t physical = 0;
|
||||
g_system->GetOutputDevice(&physical, vr::TextureType_Vulkan,
|
||||
(VkInstance_T*)instance);
|
||||
if (physical)
|
||||
{
|
||||
char buffer[4096]{};
|
||||
g_comp->GetVulkanDeviceExtensionsRequired(
|
||||
(VkPhysicalDevice_T*)(uintptr_t)physical, buffer,
|
||||
sizeof(buffer));
|
||||
device_exts = buffer;
|
||||
}
|
||||
auto destroy = (PFN_vkDestroyInstance)get_proc(
|
||||
instance, "vkDestroyInstance");
|
||||
if (destroy)
|
||||
destroy(instance, nullptr);
|
||||
}
|
||||
}
|
||||
if (device_exts.empty())
|
||||
{
|
||||
// What SteamVR asks for on every driver we know of.
|
||||
device_exts = "VK_KHR_external_memory VK_KHR_external_memory_win32 "
|
||||
"VK_KHR_external_semaphore VK_KHR_external_semaphore_win32 "
|
||||
"VK_KHR_dedicated_allocation VK_KHR_get_memory_requirements2";
|
||||
log("openvr: could not ask for the device extensions; using "
|
||||
"the usual SteamVR list");
|
||||
}
|
||||
SetEnvironmentVariableA("TRLVR_XR_VK_INSTANCE_EXTENSIONS", instance_exts);
|
||||
SetEnvironmentVariableA("TRLVR_XR_VK_DEVICE_EXTENSIONS", device_exts.c_str());
|
||||
log("openvr: Vulkan instance extensions: %s", instance_exts);
|
||||
log("openvr: Vulkan device extensions: %s", device_exts.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setup_actions()
|
||||
{
|
||||
g_input = (vr::IVRInput*)get_interface(vr::IVRInput_Version);
|
||||
if (!g_input)
|
||||
{
|
||||
log("openvr: no %s -- no controller input", vr::IVRInput_Version);
|
||||
return false;
|
||||
}
|
||||
wchar_t wide[MAX_PATH]{};
|
||||
swprintf_s(wide, L"%strlvr_actions.json", exe_dir());
|
||||
char path[MAX_PATH * 3]{};
|
||||
WideCharToMultiByte(CP_UTF8, 0, wide, -1, path, (int)sizeof(path),
|
||||
nullptr, nullptr);
|
||||
const vr::EVRInputError merr = g_input->SetActionManifestPath(path);
|
||||
if (merr != vr::VRInputError_None)
|
||||
{
|
||||
log("openvr: SetActionManifestPath(%s) failed, EVRInputError %d "
|
||||
"-- is trlvr_actions.json beside the game?", path, (int)merr);
|
||||
return false;
|
||||
}
|
||||
g_input->GetActionSetHandle("/actions/trlvr", &g_set);
|
||||
static const char* const kSide[2] = { "left", "right" };
|
||||
static const char* const kButton[XrButtonCount] = {
|
||||
"stick_click", "primary", "secondary", "trackpad", "menu",
|
||||
"trigger_touch", "thumb_touch" };
|
||||
char name[128];
|
||||
for (int h = 0; h < 2; ++h)
|
||||
{
|
||||
sprintf_s(name, "/actions/trlvr/in/stick_%s", kSide[h]);
|
||||
g_input->GetActionHandle(name, &g_stick[h]);
|
||||
sprintf_s(name, "/actions/trlvr/in/trigger_%s", kSide[h]);
|
||||
g_input->GetActionHandle(name, &g_trigger[h]);
|
||||
sprintf_s(name, "/actions/trlvr/in/squeeze_%s", kSide[h]);
|
||||
g_input->GetActionHandle(name, &g_squeeze[h]);
|
||||
for (int b = 0; b < XrButtonCount; ++b)
|
||||
{
|
||||
sprintf_s(name, "/actions/trlvr/in/%s_%s", kButton[b], kSide[h]);
|
||||
g_input->GetActionHandle(name, &g_button[b][h]);
|
||||
}
|
||||
sprintf_s(name, "/actions/trlvr/in/hand_%s", kSide[h]);
|
||||
g_input->GetActionHandle(name, &g_pose[h]);
|
||||
sprintf_s(name, "/actions/trlvr/in/hand_xr_%s", kSide[h]);
|
||||
g_input->GetActionHandle(name, &g_pose_xr[h]);
|
||||
sprintf_s(name, "/actions/trlvr/out/haptic_%s", kSide[h]);
|
||||
g_input->GetActionHandle(name, &g_haptic[h]);
|
||||
}
|
||||
log("openvr: controller actions from %s", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The controller type SteamVR reports, as the OpenXR profile the
|
||||
// rest of the mod already knows (tuning profiles, bindings logic).
|
||||
void update_profiles()
|
||||
{
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (now - g_profile_checked < 1000)
|
||||
return;
|
||||
g_profile_checked = now;
|
||||
for (int h = 0; h < 2; ++h)
|
||||
{
|
||||
const vr::TrackedDeviceIndex_t index =
|
||||
g_system->GetTrackedDeviceIndexForControllerRole(
|
||||
h == 0 ? vr::TrackedControllerRole_LeftHand
|
||||
: vr::TrackedControllerRole_RightHand);
|
||||
char type[64]{};
|
||||
if (index != vr::k_unTrackedDeviceIndexInvalid)
|
||||
g_system->GetStringTrackedDeviceProperty(index,
|
||||
vr::Prop_ControllerType_String, type, sizeof(type));
|
||||
if (strcmp(type, g_type[h]) == 0)
|
||||
continue;
|
||||
strcpy_s(g_type[h], type);
|
||||
const char* profile = "";
|
||||
XrControllerKind kind = XrControllerNone;
|
||||
if (!type[0])
|
||||
;
|
||||
else if (strstr(type, "oculus_touch"))
|
||||
{
|
||||
kind = XrControllerTouch;
|
||||
profile = "/interaction_profiles/oculus/touch_controller";
|
||||
}
|
||||
else if (strstr(type, "knuckles"))
|
||||
{
|
||||
kind = XrControllerIndex;
|
||||
profile = "/interaction_profiles/valve/index_controller";
|
||||
}
|
||||
else if (strstr(type, "hpmotioncontroller"))
|
||||
{
|
||||
kind = XrControllerHpReverb;
|
||||
profile = "/interaction_profiles/hp/mixed_reality_controller";
|
||||
}
|
||||
else if (strstr(type, "holographic"))
|
||||
{
|
||||
kind = XrControllerWmr;
|
||||
profile = "/interaction_profiles/microsoft/motion_controller";
|
||||
}
|
||||
else
|
||||
{
|
||||
kind = XrControllerSimple;
|
||||
profile = "/interaction_profiles/khr/simple_controller";
|
||||
}
|
||||
g_controller[h] = kind;
|
||||
strcpy_s(g_profile[h], profile);
|
||||
log("openvr: %s hand controller type \"%s\" (%s)",
|
||||
h == 0 ? "left" : "right", type[0] ? type : "none",
|
||||
profile[0] ? profile : "-");
|
||||
}
|
||||
}
|
||||
|
||||
bool digital(vr::VRActionHandle_t h)
|
||||
{
|
||||
if (!g_input_active || !h)
|
||||
return false;
|
||||
vr::InputDigitalActionData_t d{};
|
||||
return g_input->GetDigitalActionData(h, &d, sizeof(d),
|
||||
vr::k_ulInvalidInputValueHandle) ==
|
||||
vr::VRInputError_None && d.bActive && d.bState;
|
||||
}
|
||||
|
||||
bool analog(vr::VRActionHandle_t h, float* x, float* y)
|
||||
{
|
||||
if (!g_input_active || !h)
|
||||
return false;
|
||||
vr::InputAnalogActionData_t a{};
|
||||
if (g_input->GetAnalogActionData(h, &a, sizeof(a),
|
||||
vr::k_ulInvalidInputValueHandle) != vr::VRInputError_None ||
|
||||
!a.bActive)
|
||||
return false;
|
||||
if (x) *x = a.x;
|
||||
if (y) *y = a.y;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Every 10 s (12 reports): where SteamVR's OpenXR grip pose sits in
|
||||
// the OpenVR grip pose's own frame -- the offset between the two
|
||||
// backends' hands -- in cm (x right, y up, z back) and degrees.
|
||||
void compare_poses()
|
||||
{
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (g_pose_compare_reports >= 12 || now - g_pose_compare_at < 10000)
|
||||
return;
|
||||
g_pose_compare_at = now;
|
||||
for (int h = 0; h < 2; ++h)
|
||||
{
|
||||
vr::InputPoseActionData_t a{}, b{};
|
||||
const bool have_a = g_pose[h] &&
|
||||
g_input->GetPoseActionDataForNextFrame(g_pose[h],
|
||||
vr::TrackingUniverseSeated, &a, sizeof(a),
|
||||
vr::k_ulInvalidInputValueHandle) == vr::VRInputError_None &&
|
||||
a.bActive && a.pose.bPoseIsValid;
|
||||
const bool have_b = g_pose_xr[h] &&
|
||||
g_input->GetPoseActionDataForNextFrame(g_pose_xr[h],
|
||||
vr::TrackingUniverseSeated, &b, sizeof(b),
|
||||
vr::k_ulInvalidInputValueHandle) == vr::VRInputError_None &&
|
||||
b.bActive && b.pose.bPoseIsValid;
|
||||
if (!have_a)
|
||||
continue;
|
||||
if (!have_b)
|
||||
{
|
||||
if (g_pose_compare_reports == 0)
|
||||
log("openvr: %s hand: SteamVR gives no openxr_grip pose "
|
||||
"for this controller -- no comparison",
|
||||
h == 0 ? "left" : "right");
|
||||
continue;
|
||||
}
|
||||
const vr::HmdMatrix34_t& m = a.pose.mDeviceToAbsoluteTracking;
|
||||
const vr::HmdMatrix34_t& n = b.pose.mDeviceToAbsoluteTracking;
|
||||
float d[3], local[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
d[k] = n.m[k][3] - m.m[k][3];
|
||||
// Into the grip frame: the rotation's columns are its axes.
|
||||
for (int c = 0; c < 3; ++c)
|
||||
local[c] = m.m[0][c] * d[0] + m.m[1][c] * d[1] + m.m[2][c] * d[2];
|
||||
// Angle between the two rotations: trace of A^T B.
|
||||
float trace = 0.0f;
|
||||
for (int r = 0; r < 3; ++r)
|
||||
for (int c = 0; c < 3; ++c)
|
||||
trace += m.m[r][c] * n.m[r][c];
|
||||
const float cosine = fmaxf(-1.0f, fminf(1.0f, (trace - 1.0f) * 0.5f));
|
||||
const float angle = acosf(cosine);
|
||||
// The turn as a rotation vector in the OpenVR grip's frame:
|
||||
// R = A^T B, axis from its antisymmetric part.
|
||||
float rel[3][3];
|
||||
for (int r = 0; r < 3; ++r)
|
||||
for (int c = 0; c < 3; ++c)
|
||||
rel[r][c] = m.m[0][r] * n.m[0][c] + m.m[1][r] * n.m[1][c] +
|
||||
m.m[2][r] * n.m[2][c];
|
||||
float axis[3] = { rel[2][1] - rel[1][2], rel[0][2] - rel[2][0],
|
||||
rel[1][0] - rel[0][1] };
|
||||
const float len = sqrtf(axis[0] * axis[0] + axis[1] * axis[1] +
|
||||
axis[2] * axis[2]);
|
||||
for (float& v : axis)
|
||||
v = len > 1e-6f ? v / len * angle * 57.29578f : 0.0f;
|
||||
log("openvr: %s hand: OpenXR grip pose sits (%+.2f, %+.2f, %+.2f) cm "
|
||||
"from the OpenVR grip pose (x right, y up, z back), turned "
|
||||
"%.2f deg about (%+.2f, %+.2f, %+.2f) deg-vector; rotation rows "
|
||||
"(%+.4f %+.4f %+.4f / %+.4f %+.4f %+.4f / %+.4f %+.4f %+.4f)",
|
||||
h == 0 ? "left" : "right", local[0] * 100.0f,
|
||||
local[1] * 100.0f, local[2] * 100.0f, angle * 57.29578f,
|
||||
axis[0], axis[1], axis[2], rel[0][0], rel[0][1], rel[0][2],
|
||||
rel[1][0], rel[1][1], rel[1][2], rel[2][0], rel[2][1], rel[2][2]);
|
||||
}
|
||||
++g_pose_compare_reports;
|
||||
}
|
||||
|
||||
void copy_pose(const vr::TrackedDevicePose_t& in, TrackedPose* out)
|
||||
{
|
||||
*out = TrackedPose{};
|
||||
out->bDeviceIsConnected = in.bDeviceIsConnected;
|
||||
out->bPoseIsValid = in.bPoseIsValid;
|
||||
if (!in.bPoseIsValid)
|
||||
return;
|
||||
memcpy(out->mDeviceToAbsoluteTracking.m, in.mDeviceToAbsoluteTracking.m,
|
||||
sizeof(float) * 12);
|
||||
memcpy(out->vVelocity.v, in.vVelocity.v, sizeof(float) * 3);
|
||||
memcpy(out->vAngularVelocity.v, in.vAngularVelocity.v, sizeof(float) * 3);
|
||||
}
|
||||
}
|
||||
|
||||
// --- instance -------------------------------------------------------------
|
||||
|
||||
bool ovr_instance_ready() { return g_initialised && g_system && g_comp; }
|
||||
const char* ovr_last_error() { return g_error; }
|
||||
const char* ovr_system_name() { return g_system_name; }
|
||||
|
||||
bool ovr_instance_init()
|
||||
{
|
||||
if (ovr_instance_ready())
|
||||
return true;
|
||||
if (!load_dll())
|
||||
return false;
|
||||
if (p_IsHmdPresent && !p_IsHmdPresent())
|
||||
{
|
||||
set_error("no headset detected");
|
||||
return false;
|
||||
}
|
||||
vr::EVRInitError err = vr::VRInitError_None;
|
||||
p_InitInternal2(&err, vr::VRApplication_Scene, nullptr);
|
||||
if (err != vr::VRInitError_None)
|
||||
{
|
||||
set_error("VR_Init failed: %s",
|
||||
p_ErrorText ? p_ErrorText(err) : "unknown error");
|
||||
return false;
|
||||
}
|
||||
g_initialised = true;
|
||||
g_system = (vr::IVRSystem*)get_interface(vr::IVRSystem_Version);
|
||||
g_comp = (vr::IVRCompositor*)get_interface(vr::IVRCompositor_Version);
|
||||
if (!g_system || !g_comp)
|
||||
{
|
||||
set_error("could not get %s / %s", vr::IVRSystem_Version,
|
||||
vr::IVRCompositor_Version);
|
||||
return false;
|
||||
}
|
||||
// Seated: the origin is where the head was at the last reset, as
|
||||
// OpenXR's LOCAL space.
|
||||
g_comp->SetTrackingSpace(vr::TrackingUniverseSeated);
|
||||
|
||||
char model[128]{}, number[128]{};
|
||||
g_system->GetStringTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd,
|
||||
vr::Prop_TrackingSystemName_String, model, sizeof(model));
|
||||
g_system->GetStringTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd,
|
||||
vr::Prop_ModelNumber_String, number, sizeof(number));
|
||||
sprintf_s(g_system_name, "%s / %s", model, number);
|
||||
|
||||
publish_vulkan_extensions();
|
||||
setup_actions();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ovr_instance_shutdown()
|
||||
{
|
||||
ovr_session_destroy();
|
||||
if (g_initialised && p_ShutdownInternal)
|
||||
p_ShutdownInternal();
|
||||
g_initialised = false;
|
||||
g_system = nullptr;
|
||||
g_comp = nullptr;
|
||||
g_input = nullptr;
|
||||
g_input_active = false;
|
||||
}
|
||||
|
||||
bool ovr_recommended_size(unsigned* width, unsigned* height)
|
||||
{
|
||||
if (!ovr_instance_ready())
|
||||
return false;
|
||||
uint32_t w = 0, h = 0;
|
||||
g_system->GetRecommendedRenderTargetSize(&w, &h);
|
||||
if (width) *width = w;
|
||||
if (height) *height = h;
|
||||
return w && h;
|
||||
}
|
||||
|
||||
// --- session --------------------------------------------------------------
|
||||
|
||||
bool ovr_session_create(const XrVulkanHandles&)
|
||||
{
|
||||
g_session = ovr_instance_ready();
|
||||
if (g_session)
|
||||
log("openvr: session ready (frames go to the SteamVR compositor)");
|
||||
return g_session;
|
||||
}
|
||||
|
||||
void ovr_session_destroy()
|
||||
{
|
||||
g_session = false;
|
||||
g_frame_begun = false;
|
||||
}
|
||||
|
||||
bool ovr_session_exists() { return g_session; }
|
||||
|
||||
// --- frame ----------------------------------------------------------------
|
||||
|
||||
bool ovr_frame_wait(XrFrame* out)
|
||||
{
|
||||
if (!g_session || g_frame_begun)
|
||||
return false;
|
||||
|
||||
vr::VREvent_t event{};
|
||||
while (g_system->PollNextEvent(&event, sizeof(event)))
|
||||
{
|
||||
if (event.eventType == vr::VREvent_Quit)
|
||||
{
|
||||
log("openvr: SteamVR asked the game to quit; acknowledged");
|
||||
g_system->AcknowledgeQuit_Exiting();
|
||||
}
|
||||
}
|
||||
|
||||
LARGE_INTEGER frequency{}, before{}, after{};
|
||||
QueryPerformanceFrequency(&frequency);
|
||||
QueryPerformanceCounter(&before);
|
||||
log_set_activity("IVRCompositor::WaitGetPoses");
|
||||
const bool posed = g_comp->WaitGetPoses(g_device_poses,
|
||||
vr::k_unMaxTrackedDeviceCount, nullptr, 0) == vr::VRCompositorError_None;
|
||||
log_set_activity(nullptr);
|
||||
QueryPerformanceCounter(&after);
|
||||
g_wait_ms = double(after.QuadPart - before.QuadPart) * 1000.0 /
|
||||
double(frequency.QuadPart);
|
||||
if (!posed)
|
||||
return false;
|
||||
g_frame_begun = true;
|
||||
|
||||
g_input_active = false;
|
||||
if (g_input && g_set != vr::k_ulInvalidActionSetHandle)
|
||||
{
|
||||
vr::VRActiveActionSet_t active{};
|
||||
active.ulActionSet = g_set;
|
||||
log_set_activity("IVRInput::UpdateActionState");
|
||||
g_input_active = g_input->UpdateActionState(&active, sizeof(active), 1) ==
|
||||
vr::VRInputError_None;
|
||||
log_set_activity(nullptr);
|
||||
}
|
||||
update_profiles();
|
||||
if (g_input_active)
|
||||
compare_poses();
|
||||
|
||||
if (!out)
|
||||
return true;
|
||||
copy_pose(g_device_poses[vr::k_unTrackedDeviceIndex_Hmd],
|
||||
&out->poses[kPoseHmd]);
|
||||
for (int h = 0; h < 2; ++h)
|
||||
{
|
||||
out->poses[kPoseLeft + h] = TrackedPose{};
|
||||
vr::InputPoseActionData_t pose{};
|
||||
if (g_input_active && g_pose[h] &&
|
||||
g_input->GetPoseActionDataForNextFrame(g_pose[h],
|
||||
vr::TrackingUniverseSeated, &pose, sizeof(pose),
|
||||
vr::k_ulInvalidInputValueHandle) == vr::VRInputError_None &&
|
||||
pose.bActive)
|
||||
copy_pose(pose.pose, &out->poses[kPoseLeft + h]);
|
||||
}
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
const vr::EVREye e = i == 0 ? vr::Eye_Left : vr::Eye_Right;
|
||||
XrEyeView& eye = out->eyes[i];
|
||||
// GetProjectionRaw is the layout EyeInfo has always held.
|
||||
g_system->GetProjectionRaw(e, &eye.tan_left, &eye.tan_right,
|
||||
&eye.tan_top, &eye.tan_bottom);
|
||||
const vr::HmdMatrix34_t t = g_system->GetEyeToHeadTransform(e);
|
||||
memcpy(eye.eye_to_head.m, t.m, sizeof(float) * 12);
|
||||
}
|
||||
out->eyes_valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ovr_frame_begun() { return g_frame_begun; }
|
||||
bool ovr_frame_should_render() { return g_frame_begun; }
|
||||
double ovr_frame_wait_ms() { return g_wait_ms; }
|
||||
|
||||
// No swapchain: vr_submit hands the surface over in place (ovr_submit).
|
||||
bool ovr_swapchain_matches(unsigned, unsigned) { return true; }
|
||||
bool ovr_swapchain_create(unsigned, unsigned) { return true; }
|
||||
void ovr_swapchain_destroy() {}
|
||||
int ovr_swapchain_format() { return 0; }
|
||||
bool ovr_swapchain_acquire(uint64_t*) { return false; }
|
||||
void ovr_swapchain_release() {}
|
||||
|
||||
bool ovr_frame_end(const XrRect*, const XrRect*)
|
||||
{
|
||||
if (!g_frame_begun)
|
||||
return false;
|
||||
g_frame_begun = false;
|
||||
if (g_comp)
|
||||
g_comp->PostPresentHandoff();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ovr_submit(const OvrImage& left, const float left_bounds[4],
|
||||
const OvrImage& right, const float right_bounds[4])
|
||||
{
|
||||
if (!g_comp)
|
||||
return false;
|
||||
const OvrImage* images[2] = { &left, &right };
|
||||
const float* bounds[2] = { left_bounds, right_bounds };
|
||||
bool ok = true;
|
||||
for (int i = 0; i < 2 && ok; ++i)
|
||||
{
|
||||
const OvrImage& img = *images[i];
|
||||
vr::VRVulkanTextureData_t vk{};
|
||||
vk.m_nImage = img.image;
|
||||
vk.m_pDevice = (VkDevice_T*)img.device;
|
||||
vk.m_pPhysicalDevice = (VkPhysicalDevice_T*)img.physical_device;
|
||||
vk.m_pInstance = (VkInstance_T*)img.instance;
|
||||
vk.m_pQueue = (VkQueue_T*)img.queue;
|
||||
vk.m_nQueueFamilyIndex = img.queue_family;
|
||||
vk.m_nWidth = img.width;
|
||||
vk.m_nHeight = img.height;
|
||||
vk.m_nFormat = img.format;
|
||||
vk.m_nSampleCount = img.samples;
|
||||
vr::Texture_t tex{};
|
||||
tex.handle = &vk;
|
||||
tex.eType = vr::TextureType_Vulkan;
|
||||
tex.eColorSpace = vr::ColorSpace_Auto;
|
||||
vr::VRTextureBounds_t b{ bounds[i][0], bounds[i][1],
|
||||
bounds[i][2], bounds[i][3] };
|
||||
log_set_activity(i == 0 ? "IVRCompositor::Submit (left eye)"
|
||||
: "IVRCompositor::Submit (right eye)");
|
||||
const vr::EVRCompositorError err = g_comp->Submit(
|
||||
i == 0 ? vr::Eye_Left : vr::Eye_Right, &tex, &b,
|
||||
vr::Submit_Default);
|
||||
log_set_activity(nullptr);
|
||||
if (err != vr::VRCompositorError_None)
|
||||
{
|
||||
static int reports = 0;
|
||||
if (reports < 5)
|
||||
{
|
||||
++reports;
|
||||
log("openvr: Submit (%s eye) failed, EVRCompositorError %d",
|
||||
i == 0 ? "left" : "right", (int)err);
|
||||
}
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// --- controllers ------------------------------------------------------------
|
||||
|
||||
bool ovr_input_active() { return g_input_active; }
|
||||
|
||||
bool ovr_input_button(int hand, XrButton button)
|
||||
{
|
||||
if (hand < 0 || hand > 1 || button < 0 || button >= XrButtonCount)
|
||||
return false;
|
||||
return digital(g_button[button][hand]);
|
||||
}
|
||||
|
||||
float ovr_input_trigger(int hand)
|
||||
{
|
||||
float x = 0.0f;
|
||||
return hand >= 0 && hand <= 1 && analog(g_trigger[hand], &x, nullptr)
|
||||
? x : 0.0f;
|
||||
}
|
||||
|
||||
float ovr_input_squeeze(int hand)
|
||||
{
|
||||
return hand >= 0 && hand <= 1 && digital(g_squeeze[hand]) ? 1.0f : 0.0f;
|
||||
}
|
||||
|
||||
void ovr_input_stick(int hand, float* x, float* y)
|
||||
{
|
||||
*x = *y = 0.0f;
|
||||
if (hand >= 0 && hand <= 1)
|
||||
analog(g_stick[hand], x, y);
|
||||
}
|
||||
|
||||
bool ovr_input_haptic(int hand, float seconds, float frequency, float amplitude)
|
||||
{
|
||||
if (!g_input_active || hand < 0 || hand > 1 || !g_haptic[hand])
|
||||
return false;
|
||||
amplitude = amplitude < 0.0f ? 0.0f : amplitude > 1.0f ? 1.0f : amplitude;
|
||||
return g_input->TriggerHapticVibrationAction(g_haptic[hand], 0.0f,
|
||||
seconds, frequency, amplitude,
|
||||
vr::k_ulInvalidInputValueHandle) == vr::VRInputError_None;
|
||||
}
|
||||
|
||||
XrControllerKind ovr_input_controller(int hand)
|
||||
{
|
||||
return hand < 0 || hand > 1 ? XrControllerNone : g_controller[hand];
|
||||
}
|
||||
|
||||
const char* ovr_input_profile(int hand)
|
||||
{
|
||||
return hand < 0 || hand > 1 ? "" : g_profile[hand];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
#include "body_push.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const float kTwoPi = 6.2831853f;
|
||||
const float kDegrees = 0.017453293f;
|
||||
// The kick of a hand arriving at the speed cap shows as this share of
|
||||
// the segment's maximum lean at its peak (a critically damped spring
|
||||
// started at v0 peaks at v0 / (omega * e)).
|
||||
const float kKickPeak = 0.6f;
|
||||
const float kE = 2.7182818f;
|
||||
// Same terms as soft_body's arrival: capped at 2.5 m/s, only when
|
||||
// moving into the surface faster than 0.2 m/s.
|
||||
const float kSpeedCap = 2.5f;
|
||||
const float kSpeedMin = 0.2f;
|
||||
const float kRestAngle = 1.0e-4f; // radians
|
||||
const float kRestSpeed = 1.0e-3f; // radians per second
|
||||
|
||||
float dot(const float* a, const float* b)
|
||||
{
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||||
}
|
||||
|
||||
bool finite3(const float* a)
|
||||
{
|
||||
return std::isfinite(a[0]) && std::isfinite(a[1]) && std::isfinite(a[2]);
|
||||
}
|
||||
|
||||
float clampf(float v, float lo, float hi)
|
||||
{
|
||||
return v < lo ? lo : v > hi ? hi : v;
|
||||
}
|
||||
|
||||
// The lean after the soft limit: unchanged up to 80% of `max`, then
|
||||
// eased so it approaches `max` without a corner.
|
||||
void soft_limit(const float in[2], float max, float out[2])
|
||||
{
|
||||
const float m = std::sqrt(in[0] * in[0] + in[1] * in[1]);
|
||||
const float knee = 0.8f * max;
|
||||
if (!(max > 0.0f))
|
||||
{
|
||||
out[0] = out[1] = 0.0f;
|
||||
return;
|
||||
}
|
||||
if (m <= knee || m < 1.0e-9f)
|
||||
{
|
||||
out[0] = in[0];
|
||||
out[1] = in[1];
|
||||
return;
|
||||
}
|
||||
const float span = max - knee;
|
||||
const float limited = knee + span * std::tanh((m - knee) / span);
|
||||
out[0] = in[0] / m * limited;
|
||||
out[1] = in[1] / m * limited;
|
||||
}
|
||||
|
||||
void make_basis(BodyPushState& s, const float up[3])
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
s.up[k] = up[k];
|
||||
const float ref[3] = { std::fabs(up[0]) < 0.9f ? 1.0f : 0.0f,
|
||||
std::fabs(up[0]) < 0.9f ? 0.0f : 1.0f, 0.0f };
|
||||
const float d = dot(ref, up);
|
||||
float len = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.e1[k] = ref[k] - d * up[k];
|
||||
len += s.e1[k] * s.e1[k];
|
||||
}
|
||||
len = std::sqrt(len);
|
||||
for (int k = 0; k < 3; ++k)
|
||||
s.e1[k] /= len;
|
||||
// e2 = up x e1
|
||||
s.e2[0] = up[1] * s.e1[2] - up[2] * s.e1[1];
|
||||
s.e2[1] = up[2] * s.e1[0] - up[0] * s.e1[2];
|
||||
s.e2[2] = up[0] * s.e1[1] - up[1] * s.e1[0];
|
||||
s.valid = true;
|
||||
}
|
||||
|
||||
void clear_motion(BodyPushState& s)
|
||||
{
|
||||
std::memset(s.angle, 0, sizeof(s.angle));
|
||||
std::memset(s.vel, 0, sizeof(s.vel));
|
||||
std::memset(s.shown, 0, sizeof(s.shown));
|
||||
}
|
||||
}
|
||||
|
||||
void body_push_reset(BodyPushState& state)
|
||||
{
|
||||
state = BodyPushState();
|
||||
}
|
||||
|
||||
void body_push_step(BodyPushState& s, const BodyPushParams& params,
|
||||
const BodyPushContact* contacts, int n,
|
||||
const float pivot[kPushSegments][3], const float up_in[3],
|
||||
float metre, float dt)
|
||||
{
|
||||
float up[3] = { up_in[0], up_in[1], up_in[2] };
|
||||
const float up_len = finite3(up) ? std::sqrt(dot(up, up)) : 0.0f;
|
||||
if (!(up_len > 1.0e-6f))
|
||||
{
|
||||
body_push_reset(s);
|
||||
return;
|
||||
}
|
||||
for (float& c : up)
|
||||
c /= up_len;
|
||||
if (!s.valid || dot(up, s.up) < 0.9999f)
|
||||
{
|
||||
clear_motion(s);
|
||||
make_basis(s, up);
|
||||
}
|
||||
// Anything non-finite in the state is dropped, not propagated.
|
||||
for (int i = 0; i < kPushSegments; ++i)
|
||||
for (int k = 0; k < 2; ++k)
|
||||
if (!std::isfinite(s.angle[i][k]) || !std::isfinite(s.vel[i][k]))
|
||||
clear_motion(s);
|
||||
|
||||
float max_rad[kPushSegments];
|
||||
for (int i = 0; i < kPushSegments; ++i)
|
||||
{
|
||||
const float d = params.max_degrees[i];
|
||||
max_rad[i] = std::isfinite(d) && d > 0.0f ? d * kDegrees : 0.0f;
|
||||
}
|
||||
// A leg's foot moves reach x lean: cut the lean so it stays small.
|
||||
for (int leg = 0; leg < 2; ++leg)
|
||||
{
|
||||
const float reach = params.leg_reach_metres[leg];
|
||||
const float foot = params.foot_limit_metres;
|
||||
float cap = 0.0f;
|
||||
if (std::isfinite(reach) && std::isfinite(foot) && reach > 0.05f && foot > 0.0f)
|
||||
cap = std::asin(clampf(foot / reach, 0.0f, 1.0f));
|
||||
if (max_rad[kPushLegA + leg] > cap)
|
||||
max_rad[kPushLegA + leg] = cap;
|
||||
}
|
||||
const float strength = std::isfinite(params.strength) ? clampf(params.strength, 0.0f, 10.0f) : 0.0f;
|
||||
const float hz = std::isfinite(params.hz) ? clampf(params.hz, 0.2f, 20.0f) : 2.5f;
|
||||
const float zeta = std::isfinite(params.damping) ? clampf(params.damping, 0.05f, 3.0f) : 0.9f;
|
||||
const float omega = kTwoPi * hz;
|
||||
const bool stepping = dt > 0.0f && std::isfinite(dt) && !(dt > 1.0e6f);
|
||||
if (dt > 0.1f)
|
||||
dt = 0.1f;
|
||||
|
||||
// What the contacts ask: a target lean and a kick per segment.
|
||||
float target[kPushSegments][2] = {};
|
||||
float kick[kPushSegments][2] = {};
|
||||
// What an arm passes to the chest: its drive with the lever's sign
|
||||
// dropped (the arm end follows the push, the chest goes with it).
|
||||
float arm_target[2][2] = {};
|
||||
float arm_kick[2][2] = {};
|
||||
const bool geometry_ok = std::isfinite(metre) && metre > 0.0f && pivot;
|
||||
for (int c = 0; stepping && geometry_ok && contacts && c < n; ++c)
|
||||
{
|
||||
const BodyPushContact& ct = contacts[c];
|
||||
const int seg = ct.segment;
|
||||
if (seg < 0 || seg >= kPushSegments || !finite3(ct.point) ||
|
||||
!finite3(ct.normal) || !finite3(pivot[seg]) || !std::isfinite(ct.depth) ||
|
||||
!std::isfinite(ct.weight) || ct.weight <= 0.0f)
|
||||
continue;
|
||||
float nrm[3] = { ct.normal[0], ct.normal[1], ct.normal[2] };
|
||||
const float nl = std::sqrt(dot(nrm, nrm));
|
||||
if (!(nl > 1.0e-6f))
|
||||
continue;
|
||||
for (float& v : nrm)
|
||||
v /= nl;
|
||||
// She is pushed along -normal; only its horizontal part leans her.
|
||||
const float vert = dot(nrm, up);
|
||||
float push[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
push[k] = -(nrm[k] - vert * up[k]);
|
||||
const float a = dot(push, s.e1), b = dot(push, s.e2);
|
||||
// A push at the pivot does nothing; above it the top goes with
|
||||
// the push, below it the other way (a lever about the pivot).
|
||||
const float rel[3] = { ct.point[0] - pivot[seg][0], ct.point[1] - pivot[seg][1],
|
||||
ct.point[2] - pivot[seg][2] };
|
||||
const float height = dot(rel, up) / metre;
|
||||
float lever;
|
||||
if (seg == kPushHips)
|
||||
lever = 1.0f; // a push at the pelvis tips the body above it
|
||||
else if (seg >= kPushArmA)
|
||||
{
|
||||
// A limb: the further out, the more it swings; hanging below
|
||||
// its pivot its end follows the push (negative lever).
|
||||
const float dist = std::sqrt(dot(rel, rel)) / metre;
|
||||
const bool arm = seg <= kPushArmB;
|
||||
const float ref = arm ? params.arm_lever_metres : params.leg_lever_metres;
|
||||
lever = clampf(dist / ref, 0.0f, 1.0f) * clampf(height / 0.05f, -1.0f, 1.0f);
|
||||
}
|
||||
else
|
||||
lever = clampf(height / params.lever_metres, -0.5f, 1.0f);
|
||||
if (!std::isfinite(lever))
|
||||
continue;
|
||||
const float gain = strength * ct.weight * lever;
|
||||
const int arm_index = seg - kPushArmA;
|
||||
const bool is_arm = arm_index == 0 || arm_index == 1;
|
||||
const float abs_gain = std::fabs(gain);
|
||||
|
||||
if (ct.depth > 0.0f)
|
||||
{
|
||||
const float press = clampf(ct.depth / (params.press_metres * metre), 0.0f, 1.0f);
|
||||
const float mag = gain * max_rad[seg] * press;
|
||||
target[seg][0] += mag * a;
|
||||
target[seg][1] += mag * b;
|
||||
if (is_arm)
|
||||
{
|
||||
arm_target[arm_index][0] += abs_gain * max_rad[seg] * press * a;
|
||||
arm_target[arm_index][1] += abs_gain * max_rad[seg] * press * b;
|
||||
}
|
||||
}
|
||||
if (ct.arriving && ct.velocity_valid && finite3(ct.velocity))
|
||||
{
|
||||
const float speed = std::sqrt(dot(ct.velocity, ct.velocity)) / metre;
|
||||
if (speed > 0.0f && std::isfinite(speed))
|
||||
{
|
||||
const float scale = speed > kSpeedCap ? kSpeedCap / speed : 1.0f;
|
||||
const float into = -dot(ct.velocity, nrm) / metre * scale;
|
||||
if (into > kSpeedMin)
|
||||
{
|
||||
const float mag = gain * kKickPeak * max_rad[seg] * omega * kE *
|
||||
(into / kSpeedCap);
|
||||
kick[seg][0] += mag * a;
|
||||
kick[seg][1] += mag * b;
|
||||
if (is_arm)
|
||||
{
|
||||
const float kmag = abs_gain * kKickPeak * max_rad[seg] * omega * kE *
|
||||
(into / kSpeedCap);
|
||||
arm_kick[arm_index][0] += kmag * a;
|
||||
arm_kick[arm_index][1] += kmag * b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The head's push moves the chest a little, the chest's the waist;
|
||||
// the arms' (x arm_chain) join the chest before it passes on; the
|
||||
// waist's total moves the hips.
|
||||
auto share_of = [](float v) { return std::isfinite(v) ? clampf(v, 0.0f, 1.0f) : 0.0f; };
|
||||
for (int low = 1; low < 3; ++low)
|
||||
{
|
||||
const float share = share_of(params.chain[low - 1]);
|
||||
for (int k = 0; k < 2; ++k)
|
||||
{
|
||||
target[low][k] += share * target[low - 1][k];
|
||||
kick[low][k] += share * kick[low - 1][k];
|
||||
if (low == 1)
|
||||
for (int arm = 0; arm < 2; ++arm)
|
||||
{
|
||||
target[low][k] += share_of(params.arm_chain) * arm_target[arm][k];
|
||||
kick[low][k] += share_of(params.arm_chain) * arm_kick[arm][k];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int k = 0; k < 2; ++k)
|
||||
{
|
||||
target[kPushHips][k] += share_of(params.hips_chain) * target[kPushWaist][k];
|
||||
kick[kPushHips][k] += share_of(params.hips_chain) * kick[kPushWaist][k];
|
||||
}
|
||||
// A sustained press asks for at most the segment's maximum.
|
||||
for (int i = 0; i < kPushSegments; ++i)
|
||||
{
|
||||
const float m = std::sqrt(target[i][0] * target[i][0] + target[i][1] * target[i][1]);
|
||||
if (m > max_rad[i] && m > 0.0f)
|
||||
{
|
||||
target[i][0] *= max_rad[i] / m;
|
||||
target[i][1] *= max_rad[i] / m;
|
||||
}
|
||||
}
|
||||
|
||||
if (stepping)
|
||||
{
|
||||
for (int i = 0; i < kPushSegments; ++i)
|
||||
for (int k = 0; k < 2; ++k)
|
||||
s.vel[i][k] += kick[i][k];
|
||||
const int steps = (int)std::ceil(dt / (1.0f / 240.0f));
|
||||
const float h = dt / (float)(steps > 0 ? steps : 1);
|
||||
for (int step = 0; step < steps && step < 64; ++step)
|
||||
for (int i = 0; i < kPushSegments; ++i)
|
||||
for (int k = 0; k < 2; ++k)
|
||||
{
|
||||
const float acc = omega * omega * (target[i][k] - s.angle[i][k]) -
|
||||
2.0f * zeta * omega * s.vel[i][k];
|
||||
s.vel[i][k] += acc * h;
|
||||
s.angle[i][k] += s.vel[i][k] * h;
|
||||
}
|
||||
for (int i = 0; i < kPushSegments; ++i)
|
||||
{
|
||||
bool bad = false;
|
||||
for (int k = 0; k < 2; ++k)
|
||||
bad = bad || !std::isfinite(s.angle[i][k]) || !std::isfinite(s.vel[i][k]);
|
||||
const bool free_now = target[i][0] == 0.0f && target[i][1] == 0.0f;
|
||||
const bool at_rest = std::fabs(s.angle[i][0]) < kRestAngle &&
|
||||
std::fabs(s.angle[i][1]) < kRestAngle &&
|
||||
std::fabs(s.vel[i][0]) < kRestSpeed &&
|
||||
std::fabs(s.vel[i][1]) < kRestSpeed;
|
||||
if (bad || (free_now && at_rest))
|
||||
for (int k = 0; k < 2; ++k)
|
||||
s.angle[i][k] = s.vel[i][k] = 0.0f;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < kPushSegments; ++i)
|
||||
soft_limit(s.angle[i], max_rad[i], s.shown[i]);
|
||||
}
|
||||
|
||||
Mat4 body_push_rotation(const BodyPushState& s, int segment, float fraction)
|
||||
{
|
||||
if (!s.valid || segment < 0 || segment >= kPushSegments || !std::isfinite(fraction))
|
||||
return Mat4::identity();
|
||||
const float a = s.shown[segment][0] * fraction, b = s.shown[segment][1] * fraction;
|
||||
if (!std::isfinite(a) || !std::isfinite(b))
|
||||
return Mat4::identity();
|
||||
// The top leans toward a*e1 + b*e2: rotate about up x that
|
||||
// = a*e2 - b*e1.
|
||||
float rv[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
rv[k] = a * s.e2[k] - b * s.e1[k];
|
||||
return rotation_from_vector(rv);
|
||||
}
|
||||
|
||||
bool body_push_active(const BodyPushState& s)
|
||||
{
|
||||
for (int i = 0; i < kPushSegments; ++i)
|
||||
for (int k = 0; k < 2; ++k)
|
||||
if (std::fabs(s.angle[i][k]) > kRestAngle || std::fabs(s.vel[i][k]) > kRestSpeed)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
int body_push_segment_for_volume(const char* name, int soft, int rear_soft)
|
||||
{
|
||||
if (soft >= 0)
|
||||
return soft == rear_soft ? kPushNone : kPushChest;
|
||||
if (!name)
|
||||
return kPushNone;
|
||||
auto is = [&](const char* w) { return std::strcmp(name, w) == 0; };
|
||||
auto starts = [&](const char* w) { return std::strncmp(name, w, std::strlen(w)) == 0; };
|
||||
if (is("head") || is("neck"))
|
||||
return kPushHead;
|
||||
if (is("chest") || is("upper chest") || starts("clavicle"))
|
||||
return kPushChest;
|
||||
if (is("abdomen"))
|
||||
return kPushWaist;
|
||||
if (is("pelvis"))
|
||||
return kPushHips;
|
||||
// Limbs: the chain is the name's last letter ("forearm A").
|
||||
const size_t len = std::strlen(name);
|
||||
if (len < 2 || name[len - 2] != ' ' || (name[len - 1] != 'A' && name[len - 1] != 'B'))
|
||||
return kPushNone;
|
||||
const int chain = name[len - 1] == 'B' ? 1 : 0;
|
||||
if (starts("upper arm") || starts("forearm") || starts("hand"))
|
||||
return kPushArmA + chain;
|
||||
if (starts("thigh") || starts("shin"))
|
||||
return kPushLegA + chain;
|
||||
return kPushNone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
// General push response of Lara's body to the god hands: not a
|
||||
// wobble, just the give a body has when a hand leans on it. Three segments --
|
||||
// head, chest, waist -- each tilt a few degrees AWAY from a push, on a damped
|
||||
// spring near critical damping (no visible oscillation), and come back when
|
||||
// the hand leaves. A hand arriving fast adds a short kick on top of the
|
||||
// sustained press. Pure maths -- no config(), no game memory, no Windows
|
||||
// calls -- so mathtest links it.
|
||||
//
|
||||
// The tilt is two numbers per segment: how far its top leans along each of
|
||||
// two horizontal axes (e1, e2, built from `up`; e1 x e2 = up). A lean vector
|
||||
// (a, b) tips the segment's top toward a*e1 + b*e2 by sqrt(a^2 + b^2)
|
||||
// radians, i.e. rotates it about up x (a*e1 + b*e2). The caller turns that
|
||||
// into a rotation of the segment's bones about the segment's pivot bone
|
||||
// (body_push_rotation) -- head, chest and waist about their pivot bones (the
|
||||
// bone numbers in the comments below are Underworld's; the caller owns the
|
||||
// choice, Legend's would be neck 4, spine 2, waist 1) -- applied waist first, then chest, then head, each about
|
||||
// its pivot's position after the lower segments moved it.
|
||||
|
||||
#include "../vr_math.h"
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
enum BodyPushSegment : int
|
||||
{
|
||||
kPushNone = -1,
|
||||
kPushHead = 0, // pivot bone 17; moves bones 17..51 and 113..152
|
||||
kPushChest = 1, // pivot bone 15; moves 15..105 and 113..152
|
||||
kPushWaist = 2, // pivot bone 14; moves 14..105 and 113..152
|
||||
// AUDIT 22.6: the rest of the body. Limb segments are per CHAIN (A / B
|
||||
// as in the volume names; which is her left is decided by the caller),
|
||||
// and they hang: a push on the limb moves its END with the push, so
|
||||
// their lean is reversed against the torso's (lever < 0 below pivot).
|
||||
kPushHips = 3, // pivot bone 1; moves 1..3, 14..112 and 113..152 (not the legs)
|
||||
kPushArmA = 4, // pivot 53; moves 52..76 (clavicle a fraction, caller's choice)
|
||||
kPushArmB = 5, // pivot 78; moves 77..101
|
||||
kPushLegA = 6, // pivot 4; moves 4..8
|
||||
kPushLegB = 7, // pivot 9; moves 9..13
|
||||
kPushSegments = 8
|
||||
};
|
||||
|
||||
struct BodyPushParams
|
||||
{
|
||||
float strength = 1.0f; // x every push, 0 .. 3
|
||||
float hz = 2.5f; // spring frequency
|
||||
float damping = 0.9f; // damping ratio; ~1 = no overshoot
|
||||
// head, chest, waist, hips, arm A, arm B, leg A, leg B
|
||||
float max_degrees[kPushSegments] = { 6.0f, 4.0f, 3.0f, 3.0f, 8.0f, 8.0f, 2.0f, 2.0f };
|
||||
// A push on a segment also moves the one below it: the head's drive
|
||||
// x chain[0] goes to the chest, the chest's total x chain[1] to the
|
||||
// waist.
|
||||
float chain[2] = { 0.3f, 0.4f };
|
||||
float arm_chain = 0.2f; // each arm's drive x this goes to the chest
|
||||
float hips_chain = 0.3f; // the waist's total x this goes to the hips
|
||||
float press_metres = 0.04f; // depth that gives the full sustained tilt
|
||||
float lever_metres = 0.08f; // height above the pivot for the full lever
|
||||
// Limbs: the lever is the distance of the contact from the limb's
|
||||
// pivot over this (arm: shoulder, a hand ~0.6 m out gives the full
|
||||
// lever, an upper arm ~0.15 m about a quarter); its sign is that of
|
||||
// the contact's height relative to the pivot (below = limb end follows
|
||||
// the push), blended over +-5 cm so a level limb does not flip.
|
||||
float arm_lever_metres = 0.5f;
|
||||
float leg_lever_metres = 0.8f;
|
||||
// The feet must not visibly slide: a leg's maximum lean is cut to
|
||||
// foot_limit_metres / its reach (hip to toe, set by the caller).
|
||||
float foot_limit_metres = 0.02f;
|
||||
float leg_reach_metres[2] = { 0.9f, 0.9f }; // leg A, leg B
|
||||
};
|
||||
|
||||
struct BodyPushState
|
||||
{
|
||||
bool valid = false; // up / e1 / e2 are set
|
||||
float up[3] = { 0.0f, 0.0f, 1.0f };
|
||||
float e1[3] = { 1.0f, 0.0f, 0.0f };
|
||||
float e2[3] = { 0.0f, 1.0f, 0.0f };
|
||||
float angle[kPushSegments][2] = {}; // the springs' lean, radians
|
||||
float vel[kPushSegments][2] = {}; // radians per second
|
||||
float shown[kPushSegments][2] = {}; // the lean after the soft limit
|
||||
};
|
||||
|
||||
// One hand's contact with her body this frame.
|
||||
struct BodyPushContact
|
||||
{
|
||||
int segment = kPushNone; // which segment it pushes (or kPushNone)
|
||||
float point[3] = {}; // the contact point, world
|
||||
float normal[3] = {}; // outward surface normal (BodyHit::normal)
|
||||
float depth = 0.0f; // how far inside (BodyHit::depth), units
|
||||
float velocity[3] = {}; // the hand's velocity relative to her, units/s
|
||||
bool arriving = false; // the first frame of this contact: add the kick
|
||||
bool velocity_valid = true;
|
||||
float weight = 1.0f; // scales both terms (soft parts push lightly)
|
||||
};
|
||||
|
||||
// One frame. contacts / n: this frame's contacts (any number; each adds).
|
||||
// pivot: the head, chest and waist pivot bones' world positions. up: her
|
||||
// vertical (any length; non-finite or zero resets the state). metre: game
|
||||
// units per metre. dt in seconds; a dt of 0 only updates the output, a
|
||||
// large one is stepped in pieces up to 0.1 s. Non-finite state or input
|
||||
// is dropped, never propagated.
|
||||
void body_push_step(BodyPushState& state, const BodyPushParams& params,
|
||||
const BodyPushContact* contacts, int n,
|
||||
const float pivot[kPushSegments][3], const float up[3],
|
||||
float metre, float dt);
|
||||
|
||||
// The rotation (rotation only, no translation: turn bones about the
|
||||
// pivot with rotation_about_point) for a segment; identity at rest.
|
||||
// `fraction` scales the angle (the arm's clavicle takes a share).
|
||||
Mat4 body_push_rotation(const BodyPushState& state, int segment, float fraction = 1.0f);
|
||||
|
||||
// Any lean or speed above a tiny threshold. The step snaps what is below
|
||||
// it to exactly zero, so false means the bones need not be touched.
|
||||
bool body_push_active(const BodyPushState& state);
|
||||
|
||||
// Forget everything (she was picked up, god mode ended).
|
||||
void body_push_reset(BodyPushState& state);
|
||||
|
||||
// Which segment a body volume's hit pushes (volume names from
|
||||
// body_volumes.cpp): head, neck -> head; chest, upper chest, clavicles
|
||||
// and the chest soft boxes -> chest; abdomen -> waist; pelvis -> hips;
|
||||
// "upper arm / forearm / hand A|B" -> arm A|B; "thigh / shin A|B" -> leg
|
||||
// A|B (the chain is the volume name's last letter); feet and the buttock
|
||||
// soft box (soft index `rear_soft`) -> none. `soft` is BodyHit::soft
|
||||
// (-1 firm).
|
||||
int body_push_segment_for_volume(const char* name, int soft, int rear_soft);
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
#include "body_volumes.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Sizes in metres (a woman of ~1.75 m); every one is scaled by the
|
||||
// `metre` passed to body_volumes_build. Half extents for boxes
|
||||
// (side, front-to-back, up), radii for capsules. Tune here.
|
||||
namespace dim
|
||||
{
|
||||
const float kPelvisHalf[3] = { 0.17f, 0.11f, 0.10f };
|
||||
const float kAbdomenHalf[3] = { 0.15f, 0.10f, 0.0f }; // up: from the bones
|
||||
const float kAbdomenPad = 0.02f; // up: added to half the span
|
||||
const float kChestHalf[3] = { 0.165f, 0.11f, 0.0f }; // 0.32 wide, 0.22 deep
|
||||
const float kChestPad = 0.04f;
|
||||
const float kUpperChestHalf[3] = { 0.17f, 0.10f, 0.08f };
|
||||
// The bones run through the spine, which sits behind the body's
|
||||
// middle: the boxes are shifted forward of them.
|
||||
const float kPelvisForward = 0.01f;
|
||||
const float kAbdomenForward = 0.03f;
|
||||
const float kChestForward = 0.03f;
|
||||
const float kUpperChestForward = 0.02f;
|
||||
const float kHeadRadius = 0.095f;
|
||||
const float kNeckRadius = 0.050f;
|
||||
const float kClavicleRadius = 0.050f;
|
||||
const float kUpperArmRadius = 0.050f;
|
||||
const float kForearmRadius = 0.042f;
|
||||
const float kHandRadius = 0.050f;
|
||||
const float kHandMinLength = 0.08f; // wrist to hand centre when no fingers
|
||||
const float kThighRadius = 0.085f;
|
||||
const float kShinRadius = 0.060f;
|
||||
const float kFootRadius = 0.045f;
|
||||
// A soft part: a small box in front of the chest.
|
||||
const float kSoftHalf[3] = { 0.07f, 0.08f, 0.07f };
|
||||
const float kSoftForward = 0.06f; // in front of the joint, no tip given
|
||||
}
|
||||
|
||||
float dot(const float* a, const float* b)
|
||||
{
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||||
}
|
||||
|
||||
float length(const float* a)
|
||||
{
|
||||
return std::sqrt(dot(a, a));
|
||||
}
|
||||
|
||||
float normalise(float* a)
|
||||
{
|
||||
const float l = length(a);
|
||||
if (l > 1.0e-8f)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
a[k] /= l;
|
||||
return l;
|
||||
}
|
||||
|
||||
void cross(const float* a, const float* b, float* out)
|
||||
{
|
||||
out[0] = a[1] * b[2] - a[2] * b[1];
|
||||
out[1] = a[2] * b[0] - a[0] * b[2];
|
||||
out[2] = a[0] * b[1] - a[1] * b[0];
|
||||
}
|
||||
|
||||
struct Builder
|
||||
{
|
||||
BodyVolumes& out;
|
||||
const float* bones;
|
||||
int count;
|
||||
float metre;
|
||||
|
||||
bool has(int bone) const { return bone >= 0 && bone < count; }
|
||||
const float* pos(int bone) const { return bones + bone * 16 + 12; }
|
||||
|
||||
// An oriented box: `up` is its long axis (unit), its front is
|
||||
// the body's facing direction made square to it.
|
||||
void box(const char* name, const float centre[3], const float up_in[3],
|
||||
float forward_shift, const float half_m[3], int soft)
|
||||
{
|
||||
if (out.count >= kBodyVolumesMax)
|
||||
return;
|
||||
BodyVolume& v = out.volume[out.count];
|
||||
float up[3] = { up_in[0], up_in[1], up_in[2] };
|
||||
if (normalise(up) < 0.5f)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
up[k] = out.up[k];
|
||||
float fwd[3];
|
||||
const float along = dot(out.forward, up);
|
||||
for (int k = 0; k < 3; ++k)
|
||||
fwd[k] = out.forward[k] - along * up[k];
|
||||
if (normalise(fwd) < 0.1f)
|
||||
return; // facing along her own spine: nothing sensible
|
||||
float side[3];
|
||||
cross(up, fwd, side);
|
||||
normalise(side);
|
||||
v.kind = BodyVolumeKind::Box;
|
||||
v.name = name;
|
||||
v.soft = soft;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
v.axis[0][k] = side[k];
|
||||
v.axis[1][k] = fwd[k];
|
||||
v.axis[2][k] = up[k];
|
||||
v.centre[k] = centre[k] + fwd[k] * forward_shift * metre;
|
||||
v.half[k] = half_m[k] * metre;
|
||||
}
|
||||
++out.count;
|
||||
}
|
||||
|
||||
void capsule(const char* name, const float a[3], const float b[3], float radius_m)
|
||||
{
|
||||
if (out.count >= kBodyVolumesMax)
|
||||
return;
|
||||
BodyVolume& v = out.volume[out.count++];
|
||||
v.kind = BodyVolumeKind::Capsule;
|
||||
v.name = name;
|
||||
v.soft = -1;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
v.a[k] = a[k];
|
||||
v.b[k] = b[k];
|
||||
}
|
||||
v.radius = radius_m * metre;
|
||||
}
|
||||
|
||||
void limb(const char* name, int from, int to, float radius_m)
|
||||
{
|
||||
if (has(from) && has(to))
|
||||
capsule(name, pos(from), pos(to), radius_m);
|
||||
}
|
||||
|
||||
// Wrist to the middle of the fingers.
|
||||
void hand(const char* name, int wrist, int first, int last, int elbow)
|
||||
{
|
||||
if (!has(wrist))
|
||||
return;
|
||||
float sum[3] = {}, centre[3];
|
||||
int n = 0;
|
||||
for (int b = first; b <= last && has(b); ++b, ++n)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
sum[k] += pos(b)[k];
|
||||
float reach[3] = {};
|
||||
if (n > 0)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
centre[k] = sum[k] / (float)n;
|
||||
reach[k] = centre[k] - pos(wrist)[k];
|
||||
}
|
||||
if (n == 0 || length(reach) < 0.02f * metre)
|
||||
{
|
||||
// No fingers worth the name: carry on along the forearm.
|
||||
if (!has(elbow))
|
||||
return;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
reach[k] = pos(wrist)[k] - pos(elbow)[k];
|
||||
if (normalise(reach) < 1.0e-6f)
|
||||
return;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
centre[k] = pos(wrist)[k] + reach[k] * dim::kHandMinLength * metre;
|
||||
}
|
||||
capsule(name, pos(wrist), centre, dim::kHandRadius);
|
||||
}
|
||||
};
|
||||
|
||||
bool finite_bones(const float* bones, int count)
|
||||
{
|
||||
for (int i = 0; i < count * 16; ++i)
|
||||
if (!std::isfinite(bones[i]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Depth, outward normal, surface point of `p` against one volume;
|
||||
// `distance` is 0 inside, else the gap. Returns true when inside.
|
||||
bool probe(const BodyVolume& v, const float p[3], float* depth,
|
||||
float normal[3], float surface[3], float* distance,
|
||||
const float fallback[3])
|
||||
{
|
||||
*distance = 0.0f;
|
||||
if (v.kind == BodyVolumeKind::Ellipsoid)
|
||||
{
|
||||
// s = |local / half|: 1 on the surface. The way out is the
|
||||
// gradient; the distance to the surface its first-order
|
||||
// estimate (s - 1) / |grad s|, exact on a sphere.
|
||||
float d[3], l[3], g[3] = {}, n[3] = {};
|
||||
for (int k = 0; k < 3; ++k)
|
||||
d[k] = p[k] - v.centre[k];
|
||||
float s2 = 0.0f;
|
||||
for (int a = 0; a < 3; ++a)
|
||||
{
|
||||
l[a] = dot(d, v.axis[a]);
|
||||
s2 += (l[a] / v.half[a]) * (l[a] / v.half[a]);
|
||||
g[a] = l[a] / (v.half[a] * v.half[a]);
|
||||
}
|
||||
const float s = std::sqrt(s2);
|
||||
for (int a = 0; a < 3; ++a)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
n[k] += g[a] * v.axis[a][k];
|
||||
const float gl = normalise(n); // |l / half^2|
|
||||
float gap;
|
||||
if (gl > 1.0e-9f && s > 1.0e-6f)
|
||||
gap = (s - 1.0f) * s / gl;
|
||||
else
|
||||
{
|
||||
// At the centre: out along the thinnest axis.
|
||||
int thin = 0;
|
||||
for (int a = 1; a < 3; ++a)
|
||||
if (v.half[a] < v.half[thin])
|
||||
thin = a;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
n[k] = v.axis[thin][k];
|
||||
gap = -v.half[thin];
|
||||
}
|
||||
for (int k = 0; k < 3; ++k)
|
||||
normal[k] = n[k];
|
||||
if (s <= 1.0f)
|
||||
{
|
||||
*depth = -gap;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
surface[k] = p[k] + normal[k] * *depth;
|
||||
return true;
|
||||
}
|
||||
*distance = gap;
|
||||
*depth = -gap;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
surface[k] = p[k] - normal[k] * gap;
|
||||
return false;
|
||||
}
|
||||
if (v.kind == BodyVolumeKind::Box)
|
||||
{
|
||||
float d[3], l[3], over[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
d[k] = p[k] - v.centre[k];
|
||||
int worst = 0;
|
||||
bool inside = true;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
l[k] = dot(d, v.axis[k]);
|
||||
over[k] = std::fabs(l[k]) - v.half[k];
|
||||
if (over[k] > 0.0f)
|
||||
inside = false;
|
||||
if (over[k] > over[worst])
|
||||
worst = k;
|
||||
}
|
||||
if (inside)
|
||||
{
|
||||
// The nearest face: the way out that is shortest.
|
||||
const float sign = l[worst] < 0.0f ? -1.0f : 1.0f;
|
||||
*depth = -over[worst];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
normal[k] = v.axis[worst][k] * sign;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
surface[k] = p[k] + normal[k] * *depth;
|
||||
return true;
|
||||
}
|
||||
// Outside: out from the nearest point of the box.
|
||||
float closest[3] = { v.centre[0], v.centre[1], v.centre[2] };
|
||||
for (int a = 0; a < 3; ++a)
|
||||
{
|
||||
float c = l[a];
|
||||
c = c > v.half[a] ? v.half[a] : c < -v.half[a] ? -v.half[a] : c;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
closest[k] += v.axis[a][k] * c;
|
||||
}
|
||||
for (int k = 0; k < 3; ++k)
|
||||
normal[k] = p[k] - closest[k];
|
||||
*distance = normalise(normal);
|
||||
*depth = -*distance;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
surface[k] = closest[k];
|
||||
return false;
|
||||
}
|
||||
float ab[3], ap[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
ab[k] = v.b[k] - v.a[k];
|
||||
ap[k] = p[k] - v.a[k];
|
||||
}
|
||||
const float len2 = dot(ab, ab);
|
||||
float t = len2 > 1.0e-12f ? dot(ap, ab) / len2 : 0.0f;
|
||||
t = t < 0.0f ? 0.0f : t > 1.0f ? 1.0f : t;
|
||||
float closest[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
closest[k] = v.a[k] + ab[k] * t;
|
||||
normal[k] = p[k] - closest[k];
|
||||
}
|
||||
const float d = normalise(normal);
|
||||
if (d < 1.0e-6f)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
normal[k] = fallback[k];
|
||||
if (d < v.radius)
|
||||
{
|
||||
*depth = v.radius - d;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
surface[k] = closest[k] + normal[k] * v.radius;
|
||||
return true;
|
||||
}
|
||||
*distance = d - v.radius;
|
||||
*depth = -*distance;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
surface[k] = closest[k] + normal[k] * v.radius;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
BodyBoneMap body_bone_map_underworld()
|
||||
{
|
||||
return BodyBoneMap();
|
||||
}
|
||||
|
||||
BodyBoneMap body_bone_map_legend()
|
||||
{
|
||||
BodyBoneMap m;
|
||||
m.root = 0;
|
||||
m.pelvis = 100;
|
||||
m.pelvis2 = -1;
|
||||
m.waist = 1;
|
||||
m.chest = 2;
|
||||
m.upper_chest = 3;
|
||||
m.neck = 4;
|
||||
m.head = 5;
|
||||
const int clavicle[2] = { 58, 79 }, shoulder[2] = { 59, 80 }, elbow[2] = { 60, 81 },
|
||||
wrist[2] = { 61, 82 }, hip[2] = { 101, 105 }, knee[2] = { 102, 106 },
|
||||
ankle[2] = { 103, 107 }, toe[2] = { 104, 108 };
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
m.clavicle[i] = clavicle[i];
|
||||
m.shoulder[i] = shoulder[i];
|
||||
m.elbow[i] = elbow[i];
|
||||
m.wrist[i] = wrist[i];
|
||||
m.finger_first[i] = m.finger_last[i] = -1;
|
||||
m.hip[i] = hip[i];
|
||||
m.knee[i] = knee[i];
|
||||
m.ankle[i] = ankle[i];
|
||||
m.toe[i] = toe[i];
|
||||
}
|
||||
m.soft_count = 2;
|
||||
m.soft_bone[0] = 54; m.soft_tip[0] = 55;
|
||||
m.soft_bone[1] = 56; m.soft_tip[1] = 57;
|
||||
return m;
|
||||
}
|
||||
|
||||
void body_volumes_build(BodyVolumes& out, const float* bones, int bone_count,
|
||||
float metre, const int* soft_bones, int soft_count,
|
||||
const int* soft_tips)
|
||||
{
|
||||
BodyBoneMap map = body_bone_map_underworld();
|
||||
if (soft_count > kBodySoftMax)
|
||||
soft_count = kBodySoftMax;
|
||||
map.soft_count = soft_bones && soft_count > 0 ? soft_count : 0;
|
||||
for (int i = 0; i < map.soft_count; ++i)
|
||||
{
|
||||
map.soft_bone[i] = soft_bones[i];
|
||||
map.soft_tip[i] = soft_tips ? soft_tips[i] : -1;
|
||||
}
|
||||
body_volumes_build(out, bones, bone_count, metre, map);
|
||||
}
|
||||
|
||||
void body_volumes_build(BodyVolumes& out, const float* bones, int bone_count,
|
||||
float metre, const BodyBoneMap& map)
|
||||
{
|
||||
out = BodyVolumes();
|
||||
out.metre = metre;
|
||||
if (!bones || !(metre > 0.0f) || map.root < 0 || map.root >= bone_count ||
|
||||
!finite_bones(bones, bone_count))
|
||||
return;
|
||||
|
||||
// Her facing and vertical from the root.
|
||||
const float* root = bones + map.root * 16;
|
||||
const int fr = map.forward_row < 0 ? 0 : map.forward_row > 2 ? 2 : map.forward_row;
|
||||
const int ur = map.up_row < 0 ? 0 : map.up_row > 2 ? 2 : map.up_row;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
out.forward[k] = map.forward_sign * root[fr * 4 + k];
|
||||
out.up[k] = root[ur * 4 + k];
|
||||
}
|
||||
if (normalise(out.forward) < 0.5f || normalise(out.up) < 0.5f)
|
||||
return;
|
||||
|
||||
Builder b = { out, bones, bone_count, metre };
|
||||
if (!b.has(map.pelvis) || !b.has(map.chest) || !b.has(map.upper_chest) ||
|
||||
!b.has(map.neck) || !b.has(map.head))
|
||||
return;
|
||||
|
||||
// Soft parts first: a hand in one and in the chest behind it means
|
||||
// the soft part.
|
||||
int soft_count = map.soft_count > kBodySoftMax ? kBodySoftMax : map.soft_count;
|
||||
for (int i = 0; i < soft_count; ++i)
|
||||
{
|
||||
if (!b.has(map.soft_bone[i]))
|
||||
continue;
|
||||
const float* joint = b.pos(map.soft_bone[i]);
|
||||
float centre[3] = { joint[0], joint[1], joint[2] };
|
||||
float up[3] = { out.up[0], out.up[1], out.up[2] };
|
||||
float shift = 0.0f;
|
||||
if (b.has(map.soft_tip[i]))
|
||||
{
|
||||
// The mass is at the tip.
|
||||
for (int k = 0; k < 3; ++k)
|
||||
centre[k] = b.pos(map.soft_tip[i])[k];
|
||||
}
|
||||
else
|
||||
shift = dim::kSoftForward;
|
||||
b.box("soft", centre, up, shift, dim::kSoftHalf, i);
|
||||
}
|
||||
|
||||
// Torso: boxes along the line pelvis -> waist -> chest -> neck, each
|
||||
// standing along its stretch of it.
|
||||
float pelvis[3], up[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
pelvis[k] = b.has(map.pelvis2)
|
||||
? 0.5f * (b.pos(map.pelvis)[k] + b.pos(map.pelvis2)[k])
|
||||
: b.pos(map.pelvis)[k];
|
||||
const float* chest = b.pos(map.chest);
|
||||
const float* upper = b.pos(map.upper_chest);
|
||||
const float* neck = b.pos(map.neck);
|
||||
|
||||
for (int k = 0; k < 3; ++k)
|
||||
up[k] = (b.has(map.waist) ? b.pos(map.waist)[k] : chest[k]) - pelvis[k];
|
||||
b.box("pelvis", pelvis, up, dim::kPelvisForward, dim::kPelvisHalf, -1);
|
||||
|
||||
{
|
||||
float centre[3], span[3], half[3] = { dim::kAbdomenHalf[0], dim::kAbdomenHalf[1], 0.0f };
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
centre[k] = 0.5f * (pelvis[k] + chest[k]);
|
||||
span[k] = chest[k] - pelvis[k];
|
||||
}
|
||||
half[2] = 0.5f * length(span) / metre + dim::kAbdomenPad;
|
||||
b.box("abdomen", centre, span, dim::kAbdomenForward, half, -1);
|
||||
}
|
||||
{
|
||||
float centre[3], span[3], half[3] = { dim::kChestHalf[0], dim::kChestHalf[1], 0.0f };
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
centre[k] = 0.5f * (chest[k] + upper[k]);
|
||||
span[k] = upper[k] - chest[k];
|
||||
}
|
||||
half[2] = 0.5f * length(span) / metre + dim::kChestPad;
|
||||
b.box("chest", centre, span, dim::kChestForward, half, -1);
|
||||
}
|
||||
{
|
||||
float span[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
span[k] = neck[k] - upper[k];
|
||||
b.box("upper chest", upper, span, dim::kUpperChestForward, dim::kUpperChestHalf, -1);
|
||||
}
|
||||
|
||||
// Neck and head.
|
||||
b.limb("neck", map.upper_chest, map.neck, dim::kNeckRadius);
|
||||
{
|
||||
const float* base = neck;
|
||||
const float* top = b.pos(map.head);
|
||||
float axis[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
axis[k] = top[k] - base[k];
|
||||
const float span = normalise(axis);
|
||||
const float r = dim::kHeadRadius * metre;
|
||||
// The head's ball: its centre between a radius above the neck
|
||||
// and a radius below the top (one point if they cross).
|
||||
float a[3], c[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
a[k] = base[k] + axis[k] * r;
|
||||
c[k] = top[k] - axis[k] * r;
|
||||
}
|
||||
if (span < 2.0f * r)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
a[k] = c[k] = 0.5f * (base[k] + top[k]);
|
||||
b.capsule("head", a, c, dim::kHeadRadius);
|
||||
}
|
||||
|
||||
// Arms (A, B) and clavicles.
|
||||
static const char* const kClavicle[2] = { "clavicle A", "clavicle B" };
|
||||
static const char* const kUpperArm[2] = { "upper arm A", "upper arm B" };
|
||||
static const char* const kForearm[2] = { "forearm A", "forearm B" };
|
||||
static const char* const kHand[2] = { "hand A", "hand B" };
|
||||
static const char* const kThigh[2] = { "thigh A", "thigh B" };
|
||||
static const char* const kShin[2] = { "shin A", "shin B" };
|
||||
static const char* const kFoot[2] = { "foot A", "foot B" };
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
b.limb(kClavicle[i], map.clavicle[i], map.shoulder[i], dim::kClavicleRadius);
|
||||
b.limb(kUpperArm[i], map.shoulder[i], map.elbow[i], dim::kUpperArmRadius);
|
||||
b.limb(kForearm[i], map.elbow[i], map.wrist[i], dim::kForearmRadius);
|
||||
b.hand(kHand[i], map.wrist[i], map.finger_first[i], map.finger_last[i], map.elbow[i]);
|
||||
}
|
||||
|
||||
// Legs.
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
b.limb(kThigh[i], map.hip[i], map.knee[i], dim::kThighRadius);
|
||||
b.limb(kShin[i], map.knee[i], map.ankle[i], dim::kShinRadius);
|
||||
b.limb(kFoot[i], map.ankle[i], map.toe[i], dim::kFootRadius);
|
||||
}
|
||||
|
||||
out.valid = out.count > 0;
|
||||
}
|
||||
|
||||
bool body_volumes_test(const BodyVolumes& vols, const float point[3], BodyHit* hit)
|
||||
{
|
||||
BodyHit none;
|
||||
if (hit)
|
||||
*hit = none;
|
||||
if (!vols.valid || !std::isfinite(point[0]) || !std::isfinite(point[1]) ||
|
||||
!std::isfinite(point[2]))
|
||||
return false;
|
||||
|
||||
int best = -1, nearest = -1;
|
||||
float best_depth = 0.0f, nearest_gap = 0.0f;
|
||||
float best_normal[3] = {}, best_surface[3] = {};
|
||||
float near_normal[3] = {}, near_surface[3] = {};
|
||||
for (int i = 0; i < vols.count; ++i)
|
||||
{
|
||||
float depth, normal[3], surface[3], gap;
|
||||
if (probe(vols.volume[i], point, &depth, normal, surface, &gap, vols.forward))
|
||||
{
|
||||
// A soft part beats a firm one it sits in front of.
|
||||
const bool soft = vols.volume[i].soft >= 0;
|
||||
const bool best_soft = best >= 0 && vols.volume[best].soft >= 0;
|
||||
if (best < 0 || (soft && !best_soft) ||
|
||||
(soft == best_soft && depth > best_depth))
|
||||
{
|
||||
best = i;
|
||||
best_depth = depth;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
best_normal[k] = normal[k];
|
||||
best_surface[k] = surface[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (nearest < 0 || gap < nearest_gap)
|
||||
{
|
||||
nearest = i;
|
||||
nearest_gap = gap;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
near_normal[k] = normal[k];
|
||||
near_surface[k] = surface[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best >= 0)
|
||||
{
|
||||
if (hit)
|
||||
{
|
||||
hit->inside = true;
|
||||
hit->volume = best;
|
||||
hit->soft = vols.volume[best].soft;
|
||||
hit->depth = best_depth;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
hit->normal[k] = best_normal[k];
|
||||
hit->surface[k] = best_surface[k];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (hit && nearest >= 0)
|
||||
{
|
||||
hit->volume = nearest;
|
||||
hit->soft = vols.volume[nearest].soft;
|
||||
hit->depth = -nearest_gap;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
hit->normal[k] = near_normal[k];
|
||||
hit->surface[k] = near_surface[k];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int body_volumes_nearest_soft(const BodyVolumes& vols, const float point[3],
|
||||
float max_distance)
|
||||
{
|
||||
int pick = -1;
|
||||
float best = max_distance * max_distance;
|
||||
for (int i = 0; vols.valid && i < vols.count; ++i)
|
||||
{
|
||||
const BodyVolume& v = vols.volume[i];
|
||||
if (v.soft < 0)
|
||||
continue;
|
||||
float d2 = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
const float c = v.kind != BodyVolumeKind::Capsule
|
||||
? v.centre[k] : 0.5f * (v.a[k] + v.b[k]);
|
||||
d2 += (point[k] - c) * (point[k] - c);
|
||||
}
|
||||
if (d2 < best)
|
||||
{
|
||||
best = d2;
|
||||
pick = v.soft;
|
||||
}
|
||||
}
|
||||
return pick;
|
||||
}
|
||||
|
||||
bool body_volumes_add_box(BodyVolumes& vols, const char* name, const float centre[3],
|
||||
const float axes[3][3], const float half[3], int soft,
|
||||
BodyVolumeKind kind)
|
||||
{
|
||||
if (!vols.valid || vols.count >= kBodyVolumesMax || kind == BodyVolumeKind::Capsule)
|
||||
return false;
|
||||
for (int r = 0; r < 3; ++r)
|
||||
{
|
||||
const float len2 = axes[r][0] * axes[r][0] + axes[r][1] * axes[r][1] + axes[r][2] * axes[r][2];
|
||||
if (!(std::fabs(len2 - 1.0f) < 1e-2f) || !(half[r] > 0.0f) || !std::isfinite(centre[r]))
|
||||
return false;
|
||||
}
|
||||
BodyVolume& v = vols.volume[vols.count++];
|
||||
v = BodyVolume();
|
||||
v.kind = kind;
|
||||
v.name = name;
|
||||
v.soft = soft;
|
||||
for (int r = 0; r < 3; ++r)
|
||||
{
|
||||
v.centre[r] = centre[r];
|
||||
v.half[r] = half[r];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
v.axis[r][k] = axes[r][k];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool body_volumes_test_volume(const BodyVolumes& vols, int index, const float point[3],
|
||||
BodyHit* hit)
|
||||
{
|
||||
if (hit)
|
||||
*hit = BodyHit();
|
||||
if (!vols.valid || index < 0 || index >= vols.count || !std::isfinite(point[0]) ||
|
||||
!std::isfinite(point[1]) || !std::isfinite(point[2]))
|
||||
return false;
|
||||
float depth, normal[3], surface[3], gap;
|
||||
const bool inside = probe(vols.volume[index], point, &depth, normal, surface, &gap,
|
||||
vols.forward);
|
||||
if (hit)
|
||||
{
|
||||
hit->inside = inside;
|
||||
hit->volume = index;
|
||||
hit->soft = vols.volume[index].soft;
|
||||
hit->depth = inside ? depth : -gap;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
hit->normal[k] = normal[k];
|
||||
hit->surface[k] = surface[k];
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
// Lara's body as volumes, built each frame from her bone world matrices, so
|
||||
// a hand point can be tested against her. The torso is oriented boxes and
|
||||
// the head, neck, limbs and clavicles are capsules between joints, sized in
|
||||
// metres and scaled by `metre` (see the dim table in body_volumes.cpp --
|
||||
// tune them there).
|
||||
//
|
||||
// Pure maths: no config(), no game memory, no Windows calls.
|
||||
//
|
||||
// Which bone is which is a BodyBoneMap, so the same code serves both games.
|
||||
// Bone 0 (map.root) is the root: its axes say which way she faces and which
|
||||
// way is up, because the torso bones' own axes are not known to line up
|
||||
// with her front.
|
||||
|
||||
#include "../vr_math.h"
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
// Bone indices for body_volumes_build; -1 = absent. A and B are the two
|
||||
// chains of each pair (A = her left in the Legend map). Names follow the
|
||||
// volumes they make, which body_push_segment_for_volume reads (the last
|
||||
// letter picks the chain).
|
||||
struct BodyBoneMap
|
||||
{
|
||||
int root = 0;
|
||||
// Which row of the root's matrix is her front (taken with the sign:
|
||||
// -1 = the row negated) and which is up.
|
||||
int forward_row = 1;
|
||||
float forward_sign = -1.0f;
|
||||
int up_row = 2;
|
||||
int pelvis = 2; // the pelvis box centre is the midpoint of these two
|
||||
int pelvis2 = 1; // (-1: pelvis alone)
|
||||
int waist = 14; // the pelvis box stands along pelvis -> waist (-1: -> chest)
|
||||
int chest = 15; // abdomen top / chest bottom
|
||||
int upper_chest = 16; // chest top / upper chest centre
|
||||
int neck = 17; // the neck capsule runs upper_chest -> neck
|
||||
int head = 18; // the head's top; the capsule runs neck -> head
|
||||
int clavicle[2] = { 52, 77 };
|
||||
int shoulder[2] = { 53, 78 }; // upper arm bone (its origin is the joint)
|
||||
int elbow[2] = { 54, 79 }; // forearm bone
|
||||
int wrist[2] = { 55, 80 }; // hand bone
|
||||
int finger_first[2] = { 56, 81 }; // the hand reaches to the middle of
|
||||
int finger_last[2] = { 71, 96 }; // these (-1: along the forearm)
|
||||
int hip[2] = { 4, 9 };
|
||||
int knee[2] = { 5, 10 };
|
||||
int ankle[2] = { 6, 11 };
|
||||
int toe[2] = { 8, 13 };
|
||||
// Soft parts, indexed by soft index (BodyHit::soft). The tip is the
|
||||
// mass; -1 puts the part's box in front of the joint.
|
||||
int soft_count = 0;
|
||||
int soft_bone[8] = {};
|
||||
int soft_tip[8] = {};
|
||||
};
|
||||
|
||||
// Underworld: pelvis 1-2, waist 14, chest 15, upper chest 16, neck 17,
|
||||
// head top 18, clavicles 52 / 77, arms 53-55 / 78-80 (fingers 56-71 /
|
||||
// 81-96), legs 4-8 / 9-13. Forward is the root's -y, up its z.
|
||||
BodyBoneMap body_bone_map_underworld();
|
||||
|
||||
// Legend: root 0, waist 1, spine 2, chest 3, neck 4, head 5, collar bones
|
||||
// 58 / 79, upper arm / forearm / hand 59-61 (A, left) and 80-82 (B,
|
||||
// right), pelvis 100 (child of the waist), thigh / shin / foot / toe
|
||||
// 101-104 (A) and 105-108 (B); soft parts: breasts 54 and 56, tips 55
|
||||
// and 57. In the map the waist is Legend's bone 1, "chest" its spine (2)
|
||||
// and "upper_chest" its chest (3). No finger range: hands run along the
|
||||
// forearm. Root axes: forward = the root's row 1 negated, up = row 2,
|
||||
// as Underworld -- Legend's model axes are z up, +x her left, -y her
|
||||
// front, and bone 0's rows are those axes in the world. The game side
|
||||
// (camera_head.cpp push_map) builds its map per model from the bones'
|
||||
// structure and checks the rows against her body (logged).
|
||||
BodyBoneMap body_bone_map_legend();
|
||||
|
||||
constexpr int kBodyBonesNeeded = 153; // bones read (the skeleton's size)
|
||||
constexpr int kBodyVolumesMax = 40;
|
||||
constexpr int kBodySoftMax = 8;
|
||||
|
||||
enum class BodyVolumeKind : int { Box = 0, Capsule = 1, Ellipsoid = 2 };
|
||||
|
||||
struct BodyVolume
|
||||
{
|
||||
BodyVolumeKind kind = BodyVolumeKind::Box;
|
||||
const char* name = "";
|
||||
int soft = -1; // index into the soft parts list, -1 firm
|
||||
// Box / Ellipsoid: centre, three unit axes (side, forward, up) and half
|
||||
// extents (an ellipsoid's semi-axes).
|
||||
float centre[3] = {};
|
||||
float axis[3][3] = {};
|
||||
float half[3] = {};
|
||||
// Capsule: segment a..b and radius.
|
||||
float a[3] = {};
|
||||
float b[3] = {};
|
||||
float radius = 0.0f;
|
||||
};
|
||||
|
||||
struct BodyVolumes
|
||||
{
|
||||
bool valid = false;
|
||||
int count = 0;
|
||||
float metre = 0.0f;
|
||||
float forward[3] = {}; // the way she faces, unit
|
||||
float up[3] = {};
|
||||
BodyVolume volume[kBodyVolumesMax];
|
||||
};
|
||||
|
||||
// bones: bone_count x 16 floats (row-vector 4x4 per bone, rows 0..2
|
||||
// axes, row 3 position; game space). metre: game units per metre.
|
||||
// soft_bones: the soft parts' joint bones, soft_count of them (the
|
||||
// order is the soft index); soft_tips: each one's tip bone, or null --
|
||||
// the part's volume is then put in front of the joint. A soft part's
|
||||
// volume sits in front of the chest and is tested before the firm ones.
|
||||
// Leaves `valid` false when the bones are unusable (the map's required
|
||||
// bones out of range, non-finite, no root axes). This form overrides the
|
||||
// map's soft parts and uses the Underworld map; it is what the tests use.
|
||||
void body_volumes_build(BodyVolumes& out, const float* bones, int bone_count,
|
||||
float metre, const int* soft_bones, int soft_count,
|
||||
const int* soft_tips = nullptr);
|
||||
|
||||
// The same with an explicit bone map (soft parts from the map).
|
||||
void body_volumes_build(BodyVolumes& out, const float* bones, int bone_count,
|
||||
float metre, const BodyBoneMap& map);
|
||||
|
||||
// inside: the point is in some volume. volume: its index (-1 if none);
|
||||
// soft: that volume's soft index (-1 firm); depth: how far inside, i.e.
|
||||
// how far to push it out along `normal` (outward unit); surface: the
|
||||
// point on the surface that push reaches. When outside, volume is the
|
||||
// nearest one, depth is minus the distance to it, and normal / surface
|
||||
// describe the nearest surface point. Of the volumes holding the
|
||||
// point, a soft one wins, otherwise the deepest.
|
||||
struct BodyHit
|
||||
{
|
||||
bool inside = false;
|
||||
int volume = -1;
|
||||
int soft = -1;
|
||||
float depth = 0.0f;
|
||||
float normal[3] = {};
|
||||
float surface[3] = {};
|
||||
};
|
||||
bool body_volumes_test(const BodyVolumes& volumes, const float point[3],
|
||||
BodyHit* hit);
|
||||
|
||||
// Legend's fallback (a palm pushing up from under a breast lands in the
|
||||
// chest's box and nothing moved): the soft part whose volume centre is
|
||||
// nearest `point` and within max_distance (units), -1 if none.
|
||||
int body_volumes_nearest_soft(const BodyVolumes& volumes, const float point[3],
|
||||
float max_distance);
|
||||
|
||||
// One more soft (or firm, soft = -1) box or ellipsoid after
|
||||
// body_volumes_build: the buttock cheeks, placed by the caller (axes:
|
||||
// unit and square, rows side, forward, up; half extents / semi-axes in
|
||||
// units). False if the set is not valid or full, or the shape is unusable.
|
||||
bool body_volumes_add_box(BodyVolumes& volumes, const char* name, const float centre[3],
|
||||
const float axes[3][3], const float half[3], int soft,
|
||||
BodyVolumeKind kind = BodyVolumeKind::Box);
|
||||
|
||||
// body_volumes_test against volume `index` alone (inside / depth /
|
||||
// normal / surface as there). False when outside it or the index is bad.
|
||||
bool body_volumes_test_volume(const BodyVolumes& volumes, int index, const float point[3],
|
||||
BodyHit* hit);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
#include "rear_jiggle.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
void jiggle_step(JiggleState& s, const float target[3], float dt, const JiggleParams& p,
|
||||
float offset[3], const JiggleContact* contact)
|
||||
{
|
||||
bool reset = !s.valid || !(dt > 0.0f && dt <= 0.25f);
|
||||
float jump2 = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
if (!std::isfinite(target[k]) || !std::isfinite(s.pos[k]) || !std::isfinite(s.vel[k]))
|
||||
reset = true;
|
||||
const float d = target[k] - s.target[k];
|
||||
jump2 += d * d;
|
||||
}
|
||||
if (jump2 > p.reset_distance * p.reset_distance)
|
||||
reset = true;
|
||||
if (reset)
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.pos[k] = target[k];
|
||||
s.vel[k] = 0.0f;
|
||||
s.target[k] = target[k];
|
||||
offset[k] = 0.0f;
|
||||
}
|
||||
s.boost = 0.0f;
|
||||
s.valid = std::isfinite(target[0]) && std::isfinite(target[1]) && std::isfinite(target[2]);
|
||||
return;
|
||||
}
|
||||
// A hand on it: the limit loosens (eased in fast, out slowly), a
|
||||
// grip makes the spring stiff and near-critical so the mass goes
|
||||
// where the hand takes it, the press moves where it is pulled to,
|
||||
// and an arriving hand's kick goes into its velocity once.
|
||||
const bool touched = contact && (contact->touched || contact->gripped);
|
||||
const bool gripped = contact && contact->gripped;
|
||||
{
|
||||
const float want = touched ? 1.0f : 0.0f;
|
||||
const float tau = want > s.boost ? 0.08f : 0.5f;
|
||||
s.boost += (want - s.boost) * (1.0f - std::exp(-dt / tau));
|
||||
}
|
||||
float shift[3] = {};
|
||||
if (contact)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
if (std::isfinite(contact->shift[k]))
|
||||
shift[k] = contact->shift[k];
|
||||
if (std::isfinite(contact->kick[k]))
|
||||
s.vel[k] += contact->kick[k];
|
||||
}
|
||||
// Fixed-size sub-steps (at most 1/480 s), the target moving linearly
|
||||
// across the frame; damping acts on the velocity relative to the
|
||||
// target, so steady motion leaves no lag and only changes of motion
|
||||
// set the mass swinging.
|
||||
const float kMaxStep = 1.0f / 480.0f;
|
||||
int steps = (int)std::ceil(dt / kMaxStep);
|
||||
if (steps < 1)
|
||||
steps = 1;
|
||||
if (steps > 128)
|
||||
steps = 128;
|
||||
const float h = dt / (float)steps;
|
||||
float hz = p.hz > 0.05f ? p.hz : 0.05f, damping = p.damping > 0.0f ? p.damping : 0.0f;
|
||||
if (gripped)
|
||||
{
|
||||
hz = hz * 2.0f > 6.0f ? hz * 2.0f : 6.0f;
|
||||
damping = damping > 0.9f ? damping : 0.9f;
|
||||
}
|
||||
const float omega = 6.2831853f * hz;
|
||||
const float k_spring = omega * omega;
|
||||
const float c_damp = 2.0f * damping * omega;
|
||||
float vt[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
vt[k] = (target[k] - s.target[k]) / dt;
|
||||
// Past the limit the tissue stiffens: an extra spring (kStiffen x
|
||||
// the base one) and a near-critical damper on outward motion bring a
|
||||
// hard swing to a rounded stop instead of a wall. The mass itself is
|
||||
// held within three times the limit only as a safety for hitches the
|
||||
// reset misses; what is shown eases toward the limit (below).
|
||||
const float shown_limit = (p.max_offset > 0.0f ? p.max_offset : 0.0f) +
|
||||
s.boost * (p.touch_offset > 0.0f ? p.touch_offset : 0.0f);
|
||||
const float limit = 3.0f * shown_limit;
|
||||
const float kStiffen = 6.0f;
|
||||
const float k_stiff = kStiffen * k_spring;
|
||||
const float c_stiff = 2.0f * std::sqrt(kStiffen) * omega;
|
||||
for (int i = 1; i <= steps; ++i)
|
||||
{
|
||||
// The force from the sub-step's starting target (semi-implicit
|
||||
// Euler then leaves no lag behind a steadily moving target), the
|
||||
// limit against its end.
|
||||
const float f0 = (float)(i - 1) / (float)steps, f = (float)i / (float)steps;
|
||||
float tgt[3], d[3], t0[3], a[3];
|
||||
float lag0[3], lag0_len2 = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
t0[k] = s.target[k] + (target[k] - s.target[k]) * f0;
|
||||
tgt[k] = s.target[k] + (target[k] - s.target[k]) * f;
|
||||
a[k] = k_spring * (t0[k] + shift[k] - s.pos[k]) - c_damp * (s.vel[k] - vt[k]);
|
||||
lag0[k] = s.pos[k] - t0[k];
|
||||
lag0_len2 += lag0[k] * lag0[k];
|
||||
}
|
||||
const float lag0_len = std::sqrt(lag0_len2);
|
||||
if (shown_limit > 0.0f && lag0_len > shown_limit)
|
||||
{
|
||||
float out_v = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
out_v += (s.vel[k] - vt[k]) * lag0[k] / lag0_len;
|
||||
const float push = k_stiff * (lag0_len - shown_limit) +
|
||||
(out_v > 0.0f ? c_stiff * out_v : 0.0f);
|
||||
for (int k = 0; k < 3; ++k)
|
||||
a[k] -= push * lag0[k] / lag0_len;
|
||||
}
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.vel[k] += a[k] * h;
|
||||
s.pos[k] += s.vel[k] * h;
|
||||
d[k] = s.pos[k] - tgt[k];
|
||||
}
|
||||
const float len = std::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
|
||||
if (len > limit)
|
||||
{
|
||||
// On the limit: held there, the outward part of the relative
|
||||
// velocity dropped.
|
||||
const float scale = len > 0.0f ? limit / len : 0.0f;
|
||||
float out_v = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
out_v += (s.vel[k] - vt[k]) * (len > 0.0f ? d[k] / len : 0.0f);
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.pos[k] = tgt[k] + d[k] * scale;
|
||||
if (out_v > 0.0f && len > 0.0f)
|
||||
s.vel[k] -= out_v * d[k] / len;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Shown: the lag eased toward the limit (limit x tanh(len / limit):
|
||||
// linear when small, smooth, never past it), so a gait that swings
|
||||
// the mass hard rounds off instead of stopping at a wall.
|
||||
float lag[3], len2 = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
s.target[k] = target[k];
|
||||
lag[k] = s.pos[k] - target[k];
|
||||
len2 += lag[k] * lag[k];
|
||||
}
|
||||
const float len = std::sqrt(len2);
|
||||
const float scale = len > 1e-9f && shown_limit > 0.0f
|
||||
? shown_limit * std::tanh(len / shown_limit) / len : 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
offset[k] = lag[k] * scale;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
// A point mass on a damped spring towards a moving target: the 3D jiggle of
|
||||
// the buttocks (ported from the Underworld mod's skin_deform; the shader
|
||||
// patcher is not part of it, Legend skins on the CPU). Pure maths -- no
|
||||
// config(), no game memory, no Windows calls -- so mathtest links it.
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
// A point mass on a damped spring towards a moving target.
|
||||
struct JiggleParams
|
||||
{
|
||||
float hz = 3.0f; // natural frequency
|
||||
float damping = 0.3f; // damping ratio (1 = critical)
|
||||
float max_offset = 1.0f; // the shown offset eases toward this (units)
|
||||
float touch_offset = 0.0f; // ... plus this while a hand is on it (eased)
|
||||
float reset_distance = 100.0f; // a target jump this large in one step resets (units)
|
||||
};
|
||||
|
||||
struct JiggleState
|
||||
{
|
||||
bool valid = false;
|
||||
float pos[3] = {};
|
||||
float vel[3] = {};
|
||||
float target[3] = {}; // last step's target
|
||||
float boost = 0.0f; // 0..1: a hand on it (eased in 0.08 s, out 0.5 s)
|
||||
};
|
||||
|
||||
// A hand on the mass this step (god mode; rear::soft_contact in
|
||||
// soft_body.h gathers the press and kick).
|
||||
struct JiggleContact
|
||||
{
|
||||
bool touched = false; // a hand is on it: the limit loosens
|
||||
bool gripped = false; // a closed grip holds it: stiff, near-critical
|
||||
float shift[3] = {}; // where the hand has taken it: added to the target (units)
|
||||
float kick[3] = {}; // velocity added once, this step (units/s)
|
||||
};
|
||||
|
||||
// Advances by dt seconds (fixed sub-steps, so the result does not depend
|
||||
// on the frame rate) and returns the mass's offset from the target
|
||||
// (world units), eased toward the limit (limit x tanh(lag / limit),
|
||||
// limit = max_offset + boost x touch_offset); past the limit the spring
|
||||
// stiffens (and damps outward motion) so a hard swing rounds off, and the
|
||||
// mass is held within three times it. A first step, a dt outside (0, 0.25] or a target
|
||||
// jump beyond reset_distance puts the mass on the target at rest.
|
||||
// contact may be null (no hand).
|
||||
void jiggle_step(JiggleState& s, const float target[3], float dt, const JiggleParams& p,
|
||||
float offset[3], const JiggleContact* contact = nullptr);
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
#include "rear_shape.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace rear
|
||||
{
|
||||
namespace
|
||||
{
|
||||
float clampf(float v, float lo, float hi)
|
||||
{
|
||||
return v < lo ? lo : (v > hi ? hi : v);
|
||||
}
|
||||
|
||||
float smooth01(float u)
|
||||
{
|
||||
u = clampf(u, 0.0f, 1.0f);
|
||||
return u * u * (3.0f - 2.0f * u);
|
||||
}
|
||||
|
||||
// The fit's constants, metres.
|
||||
const float kFitZLo = -0.20f, kFitZHi = -0.02f; // below the belt, above the thigh
|
||||
const float kFitXLo = 0.015f, kFitXHi = 0.17f; // off the midline, inside the hip
|
||||
const float kCapDepth = 0.025f; // skin within this of the peak
|
||||
const float kDepthRadius = 0.09f;
|
||||
const float kCentreInside = 0.42f; // x kDepthRadius inside the peak
|
||||
const float kTouchBehind = 0.01f; // touch zone's back edge past the peak
|
||||
const float kTouchSide = 0.02f; // ... and past the cap at the sides
|
||||
const float kTouchDepth = 0.08f; // its depth semi-axis
|
||||
|
||||
// The nominal cheek (metres) when her mesh does not fit.
|
||||
void nominal(int side, RearFit& out)
|
||||
{
|
||||
const float sign = side ? 1.0f : -1.0f;
|
||||
out.peak[side][0] = sign * 0.085f;
|
||||
out.peak[side][1] = 0.108f;
|
||||
out.peak[side][2] = -0.09f;
|
||||
out.cap_lo[side][0] = sign > 0.0f ? 0.03f : -0.14f;
|
||||
out.cap_hi[side][0] = sign > 0.0f ? 0.14f : -0.03f;
|
||||
out.cap_lo[side][1] = -0.15f;
|
||||
out.cap_hi[side][1] = -0.03f;
|
||||
out.centre[side][0] = sign * 0.085f;
|
||||
out.centre[side][1] = 0.108f - kCentreInside * kDepthRadius;
|
||||
out.centre[side][2] = -0.09f;
|
||||
out.radius[side][0] = 0.09f;
|
||||
out.radius[side][1] = kDepthRadius;
|
||||
out.radius[side][2] = 0.10f;
|
||||
}
|
||||
|
||||
void scale_fit(RearFit& out, float metre)
|
||||
{
|
||||
for (int s = 0; s < 2; ++s)
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
out.peak[s][k] *= metre;
|
||||
out.centre[s][k] *= metre;
|
||||
out.radius[s][k] *= metre;
|
||||
}
|
||||
for (int k = 0; k < 2; ++k)
|
||||
{
|
||||
out.cap_lo[s][k] *= metre;
|
||||
out.cap_hi[s][k] *= metre;
|
||||
}
|
||||
}
|
||||
out.seam_x *= metre;
|
||||
}
|
||||
}
|
||||
|
||||
float region_weight(const RearRegion& r, const float p[3])
|
||||
{
|
||||
float d2 = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
const float inv = r.radius[k] > 1e-3f ? 1.0f / r.radius[k] : 1e3f;
|
||||
const float t = (p[k] - r.centre[k]) * inv;
|
||||
d2 += t * t;
|
||||
}
|
||||
const float core = clampf(r.core, 0.0f, 0.95f);
|
||||
return smooth01((1.0f - d2) / (1.0f - core * core));
|
||||
}
|
||||
|
||||
float seam_weight(float x, float seam_x, float seam_soft, int side)
|
||||
{
|
||||
if (!(seam_soft > 0.0f))
|
||||
return 1.0f;
|
||||
const float sign = side ? 1.0f : -1.0f;
|
||||
return smooth01(0.5f + sign * (x - seam_x) / (2.0f * seam_soft));
|
||||
}
|
||||
|
||||
bool dent_push(float p[3], const float centre[3], float radius)
|
||||
{
|
||||
if (!(radius > 0.0f))
|
||||
return false;
|
||||
float d[3], len2 = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
d[k] = p[k] - centre[k];
|
||||
len2 += d[k] * d[k];
|
||||
}
|
||||
// As the shader: eps keeps the direction finite (a point on the
|
||||
// centre stays where it is).
|
||||
const float len = std::sqrt(len2 + 1e-6f);
|
||||
const float in = radius - len;
|
||||
if (!(in > 0.0f))
|
||||
return false;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
p[k] += d[k] / len * in;
|
||||
return true;
|
||||
}
|
||||
|
||||
RearWeights rear_weights(const RearShape& s, const float p[3])
|
||||
{
|
||||
RearWeights w;
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
const float r = region_weight(s.cheek[i], p);
|
||||
w.cheek[i] = r > 0.0f ? r * seam_weight(p[0], s.seam_x, s.seam_soft, i) : 0.0f;
|
||||
}
|
||||
w.cleft = region_weight(s.cleft, p);
|
||||
return w;
|
||||
}
|
||||
|
||||
void rear_deform(const float bind[3], const RearWeights& w, const float offset[2][3],
|
||||
const float cleft_offset[3], const RearDentSphere* dents, int dent_count,
|
||||
float out[3])
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
out[k] = bind[k] + w.cheek[0] * offset[0][k] + w.cheek[1] * offset[1][k] +
|
||||
w.cleft * cleft_offset[k];
|
||||
for (int j = 0; dents && j < dent_count; ++j)
|
||||
if (dents[j].on)
|
||||
dent_push(out, dents[j].centre, dents[j].radius);
|
||||
}
|
||||
|
||||
void rear_inflate(const RearShape& s, const RearWeights& w, const float p[3], float amount,
|
||||
float delta[3])
|
||||
{
|
||||
if (!std::isfinite(amount) || amount == 0.0f)
|
||||
return;
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
if (!(w.cheek[i] > 0.0f))
|
||||
continue;
|
||||
const RearRegion& r = s.cheek[i];
|
||||
float d[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
d[k] = (p[k] - r.centre[k]) / fmaxf(r.radius[k], 1e-3f);
|
||||
d[1] += 0.6f; // toward her back
|
||||
const float len = std::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
|
||||
if (!(len > 1e-4f))
|
||||
continue;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
delta[k] += w.cheek[i] * amount * d[k] / len;
|
||||
}
|
||||
}
|
||||
|
||||
void soft_limit_vector(float v[3], float limit)
|
||||
{
|
||||
if (!(limit > 0.0f))
|
||||
return;
|
||||
const float len = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
||||
if (!std::isfinite(len))
|
||||
{
|
||||
v[0] = v[1] = v[2] = 0.0f;
|
||||
return;
|
||||
}
|
||||
// limit x tanh(len / limit) all the way (as the chest's soft parts):
|
||||
// one to one when small, bending gradually from about half the
|
||||
// limit, never reaching it. A knee late in the range (it was 0.8)
|
||||
// squeezes everything past it into the last few millimetres, so a
|
||||
// big swing looks as if it hit a wall.
|
||||
if (!(len > 1e-9f))
|
||||
return;
|
||||
const float scale = limit * std::tanh(len / limit) / len;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
v[k] *= scale;
|
||||
}
|
||||
|
||||
short round_short(float v)
|
||||
{
|
||||
if (!std::isfinite(v))
|
||||
return 0;
|
||||
const float r = std::floor(v + 0.5f);
|
||||
return (short)clampf(r, -32768.0f, 32767.0f);
|
||||
}
|
||||
|
||||
bool rear_fit(const float* points, int count, float metre, RearFit& out)
|
||||
{
|
||||
out = RearFit();
|
||||
if (!(metre > 0.0f))
|
||||
metre = 291.0f;
|
||||
const float inv = 1.0f / metre;
|
||||
bool ok = points && count > 0;
|
||||
for (int side = 0; side < 2 && ok; ++side)
|
||||
{
|
||||
const float sign = side ? 1.0f : -1.0f;
|
||||
const auto use = [&](const float* p) -> bool
|
||||
{
|
||||
const float x = sign * p[0] * inv, y = p[1] * inv, z = p[2] * inv;
|
||||
return std::isfinite(x) && std::isfinite(y) && std::isfinite(z) &&
|
||||
x >= kFitXLo && x <= kFitXHi && z >= kFitZLo && z <= kFitZHi && y > 0.0f;
|
||||
};
|
||||
int n = 0;
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (use(points + 3 * i))
|
||||
++n;
|
||||
out.samples[side] = n;
|
||||
if (n < 12)
|
||||
{
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
// The peak: the mean of the few rearmost points (one stray
|
||||
// vertex does not make it).
|
||||
const int K = n / 20 < 3 ? 3 : (n / 20 > 8 ? 8 : n / 20);
|
||||
int top[8];
|
||||
int have = 0;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
if (!use(points + 3 * i))
|
||||
continue;
|
||||
const float y = points[3 * i + 1];
|
||||
if (have < K)
|
||||
{
|
||||
top[have++] = i;
|
||||
continue;
|
||||
}
|
||||
int low = 0;
|
||||
for (int t = 1; t < have; ++t)
|
||||
if (points[3 * top[t] + 1] < points[3 * top[low] + 1])
|
||||
low = t;
|
||||
if (y > points[3 * top[low] + 1])
|
||||
top[low] = i;
|
||||
}
|
||||
float peak[3] = {};
|
||||
for (int t = 0; t < have; ++t)
|
||||
for (int k = 0; k < 3; ++k)
|
||||
peak[k] += points[3 * top[t] + k] * inv / (float)have;
|
||||
if (peak[1] < 0.03f || peak[1] > 0.25f)
|
||||
{
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
// The cap: the skin within kCapDepth of the peak's depth.
|
||||
float xlo = 1e9f, xhi = -1e9f, zlo = 1e9f, zhi = -1e9f;
|
||||
int cap = 0;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
const float* p = points + 3 * i;
|
||||
if (!use(p) || p[1] * inv < peak[1] - kCapDepth)
|
||||
continue;
|
||||
++cap;
|
||||
xlo = fminf(xlo, p[0] * inv);
|
||||
xhi = fmaxf(xhi, p[0] * inv);
|
||||
zlo = fminf(zlo, p[2] * inv);
|
||||
zhi = fmaxf(zhi, p[2] * inv);
|
||||
}
|
||||
if (cap < 4)
|
||||
{
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
for (int k = 0; k < 3; ++k)
|
||||
out.peak[side][k] = peak[k];
|
||||
out.cap_lo[side][0] = xlo;
|
||||
out.cap_hi[side][0] = xhi;
|
||||
out.cap_lo[side][1] = zlo;
|
||||
out.cap_hi[side][1] = zhi;
|
||||
out.centre[side][0] = 0.5f * (xlo + xhi);
|
||||
out.centre[side][1] = peak[1] - kCentreInside * kDepthRadius;
|
||||
// Centred on the peak's height, not the cap's middle: the cap
|
||||
// runs down the back of the thigh (the simulator log had its
|
||||
// middle 4.5 cm below the peak, a region sitting on her leg).
|
||||
out.centre[side][2] = peak[2];
|
||||
out.radius[side][0] = clampf(1.5f * 0.5f * (xhi - xlo), 0.05f, 0.14f);
|
||||
out.radius[side][1] = kDepthRadius;
|
||||
out.radius[side][2] = clampf(1.5f * 0.5f * (zhi - zlo), 0.06f, 0.15f);
|
||||
}
|
||||
if (ok)
|
||||
out.seam_x = clampf(0.5f * (out.centre[0][0] + out.centre[1][0]), -0.02f, 0.02f);
|
||||
else
|
||||
{
|
||||
const int s0 = out.samples[0], s1 = out.samples[1];
|
||||
out = RearFit();
|
||||
out.samples[0] = s0;
|
||||
out.samples[1] = s1;
|
||||
out.fallback = true;
|
||||
nominal(0, out);
|
||||
nominal(1, out);
|
||||
out.seam_x = 0.0f;
|
||||
}
|
||||
out.ok = ok;
|
||||
scale_fit(out, metre);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void rear_shape_from_fit(const RearFit& fit, const RearAdjust& adj, float metre, RearShape& out)
|
||||
{
|
||||
const float cm = metre * 0.01f;
|
||||
const float shape[3] = { adj.size * adj.width, adj.size, adj.size * adj.height };
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
const float sign = i ? 1.0f : -1.0f;
|
||||
RearRegion& r = out.cheek[i];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
r.centre[k] = fit.centre[i][k];
|
||||
r.radius[k] = fmaxf(fit.radius[i][k] * shape[k], 0.5f * cm);
|
||||
}
|
||||
r.centre[0] += sign * (adj.spread_cm - kSpreadDefaultCm) * cm;
|
||||
r.centre[2] += adj.lift_cm * cm;
|
||||
r.core = clampf(adj.core, 0.0f, 0.95f);
|
||||
}
|
||||
out.seam_x = fit.seam_x;
|
||||
out.seam_soft = adj.seam_cm > 0.0f ? adj.seam_cm * cm : 0.0f;
|
||||
// The cleft: a narrow band round her midline over the cheeks' depth
|
||||
// and height.
|
||||
RearRegion& c = out.cleft;
|
||||
c.centre[0] = fit.seam_x;
|
||||
for (int k = 1; k < 3; ++k)
|
||||
{
|
||||
c.centre[k] = 0.5f * (out.cheek[0].centre[k] + out.cheek[1].centre[k]);
|
||||
c.radius[k] = 0.5f * (out.cheek[0].radius[k] + out.cheek[1].radius[k]);
|
||||
}
|
||||
c.radius[0] = fmaxf(adj.cleft_width_cm, 0.5f) * cm;
|
||||
c.core = 0.3f;
|
||||
}
|
||||
|
||||
void rear_touch_zone(const RearFit& fit, const RearAdjust& adj, int side, float metre,
|
||||
float touch_cm, float centre[3], float semi[3])
|
||||
{
|
||||
side = side ? 1 : 0;
|
||||
const float cm = metre * 0.01f;
|
||||
const float sign = side ? 1.0f : -1.0f;
|
||||
centre[0] = 0.5f * (fit.cap_lo[side][0] + fit.cap_hi[side][0]) +
|
||||
sign * (adj.spread_cm - kSpreadDefaultCm) * cm;
|
||||
centre[1] = fit.peak[side][1] + (kTouchBehind - kTouchDepth) * metre;
|
||||
centre[2] = 0.5f * (fit.cap_lo[side][1] + fit.cap_hi[side][1]) + adj.lift_cm * cm;
|
||||
semi[0] = 0.5f * (fit.cap_hi[side][0] - fit.cap_lo[side][0]) + kTouchSide * metre;
|
||||
semi[1] = kTouchDepth * metre;
|
||||
semi[2] = 0.5f * (fit.cap_hi[side][1] - fit.cap_lo[side][1]) + kTouchSide * metre;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
semi[k] = fmaxf(semi[k] + touch_cm * cm, 2.0f * cm);
|
||||
}
|
||||
|
||||
bool ellipsoid_contact(const float p[3], const float centre[3], const float semi[3],
|
||||
float margin, float* depth, float normal[3])
|
||||
{
|
||||
float l[3], h[3], s2 = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
l[k] = p[k] - centre[k];
|
||||
h[k] = fmaxf(semi[k] + margin, 1e-3f);
|
||||
s2 += (l[k] / h[k]) * (l[k] / h[k]);
|
||||
}
|
||||
if (!(s2 <= 1.0f))
|
||||
return false;
|
||||
float g[3], g2 = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
g[k] = l[k] / (h[k] * h[k]);
|
||||
g2 += g[k] * g[k];
|
||||
}
|
||||
const float glen = std::sqrt(g2);
|
||||
if (glen > 1e-12f)
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
normal[k] = g[k] / glen;
|
||||
*depth = (1.0f - s2) / (2.0f * glen);
|
||||
}
|
||||
else
|
||||
{
|
||||
normal[0] = 0.0f;
|
||||
normal[1] = 1.0f; // the centre: out the back
|
||||
normal[2] = 0.0f;
|
||||
*depth = fminf(h[0], fminf(h[1], h[2]));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool world_dir_to_local(const float m[16], const float v[3], float out[3])
|
||||
{
|
||||
for (int r = 0; r < 3; ++r)
|
||||
{
|
||||
const float* row = m + 4 * r;
|
||||
const float len2 = row[0] * row[0] + row[1] * row[1] + row[2] * row[2];
|
||||
if (!(len2 > 1e-12f) || !std::isfinite(len2))
|
||||
return false;
|
||||
out[r] = (v[0] * row[0] + v[1] * row[1] + v[2] * row[2]) / len2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool world_point_to_local(const float m[16], const float p[3], float out[3])
|
||||
{
|
||||
const float d[3] = { p[0] - m[12], p[1] - m[13], p[2] - m[14] };
|
||||
return world_dir_to_local(m, d, out);
|
||||
}
|
||||
|
||||
void local_point_to_world(const float m[16], const float p[3], float out[3])
|
||||
{
|
||||
for (int j = 0; j < 3; ++j)
|
||||
out[j] = p[0] * m[j] + p[1] * m[4 + j] + p[2] * m[8 + j] + m[12 + j];
|
||||
}
|
||||
|
||||
void local_dir_to_world(const float m[16], const float v[3], float out[3])
|
||||
{
|
||||
for (int j = 0; j < 3; ++j)
|
||||
out[j] = v[0] * m[j] + v[1] * m[4 + j] + v[2] * m[8 + j];
|
||||
}
|
||||
|
||||
float matrix_scale(const float m[16])
|
||||
{
|
||||
float sum = 0.0f;
|
||||
for (int r = 0; r < 3; ++r)
|
||||
{
|
||||
const float* row = m + 4 * r;
|
||||
const float len = std::sqrt(row[0] * row[0] + row[1] * row[1] + row[2] * row[2]);
|
||||
if (!(len > 1e-6f) || !std::isfinite(len))
|
||||
return 0.0f;
|
||||
sum += len;
|
||||
}
|
||||
return sum / 3.0f;
|
||||
}
|
||||
|
||||
void cheek_offset(const float lag_local[3], const float gain[3], float boost, float inward,
|
||||
float test_push, float out[3])
|
||||
{
|
||||
const float b = clampf(boost, 0.0f, 1.0f);
|
||||
for (int r = 0; r < 3; ++r)
|
||||
{
|
||||
float g = gain[r] + (1.0f - gain[r]) * b;
|
||||
if (r == 1 && lag_local[r] < 0.0f)
|
||||
g *= inward;
|
||||
out[r] = lag_local[r] * g;
|
||||
}
|
||||
out[1] += test_push;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
#pragma once
|
||||
// The buttocks' shape for the rear soft body: which of Lara's source
|
||||
// vertices move, by how much, and where a fingertip dents them. The
|
||||
// falloff is the Underworld mod's shader maths (skin_deform: a smoothstep
|
||||
// ellipsoid with a full-weight core, a seam ramp across her midline, a
|
||||
// cleft band, fingertip dent spheres), run here on the CPU over the mesh's
|
||||
// int16 source vertices, which Legend skins on every draw. Also the fit of
|
||||
// the cheeks to her measured mesh, and the touch zones god mode tests.
|
||||
//
|
||||
// Space: "pelvis-local bind" -- model units (about 291 per metre), axes as
|
||||
// the model's (z up, +x her left, +y back, -y front), origin at the pelvis
|
||||
// bone's bind joint. Legend's bind frames are not rotated, so an offset in
|
||||
// this space added to a vertex's local shorts is an offset before skinning.
|
||||
//
|
||||
// Pure maths -- no config(), no game memory, no Windows calls -- so
|
||||
// mathtest links it.
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace rear
|
||||
{
|
||||
// One region: an ellipsoid whose weight is 1 inside `core` (a fraction
|
||||
// of the radius) and eases to 0 at its surface with a smoothstep.
|
||||
struct RearRegion
|
||||
{
|
||||
float centre[3] = {};
|
||||
float radius[3] = { 1.0f, 1.0f, 1.0f };
|
||||
float core = 0.0f; // 0 .. 0.95
|
||||
};
|
||||
|
||||
// u = saturate((1 - d^2) / (1 - core^2)), d^2 = sum ((p - c) / r)^2;
|
||||
// returns u^2 (3 - 2u). 1 at the centre and inside the core, 0 at and
|
||||
// beyond the surface, no crease at either edge.
|
||||
float region_weight(const RearRegion& r, const float p[3]);
|
||||
|
||||
// The seam cut: side 0 (her -x cheek) keeps x < seam_x, side 1 keeps
|
||||
// x > seam_x; 0.5 on the seam, 0 / 1 at seam_soft beyond it (a
|
||||
// smoothstep in between). seam_soft <= 0: no cut (1 everywhere).
|
||||
float seam_weight(float x, float seam_x, float seam_soft, int side);
|
||||
|
||||
// A fingertip dent: a point inside the sphere is pushed out along the
|
||||
// line from its centre to its surface (P += normalize(P - c) x
|
||||
// max(r - |P - c|, 0)). Returns whether it moved.
|
||||
bool dent_push(float p[3], const float centre[3], float radius);
|
||||
|
||||
// Both cheeks (0: -x, 1: +x), the cleft between them and the seam.
|
||||
struct RearShape
|
||||
{
|
||||
RearRegion cheek[2];
|
||||
RearRegion cleft;
|
||||
float seam_x = 0.0f;
|
||||
float seam_soft = 0.0f; // 0 = no cut
|
||||
};
|
||||
|
||||
struct RearWeights
|
||||
{
|
||||
float cheek[2] = { 0.0f, 0.0f };
|
||||
float cleft = 0.0f;
|
||||
};
|
||||
|
||||
// Per vertex (bind position): each cheek's region weight times its
|
||||
// seam ramp; the cleft is never cut.
|
||||
RearWeights rear_weights(const RearShape& s, const float p[3]);
|
||||
|
||||
struct RearDentSphere
|
||||
{
|
||||
bool on = false;
|
||||
float centre[3] = {};
|
||||
float radius = 0.0f;
|
||||
};
|
||||
|
||||
// The moved bind position of one vertex: bind + w0 x offset0 + w1 x
|
||||
// offset1 + wc x cleft_offset, then each dent that is on.
|
||||
void rear_deform(const float bind[3], const RearWeights& w, const float offset[2][3],
|
||||
const float cleft_offset[3], const RearDentSphere* dents, int dent_count,
|
||||
float out[3]);
|
||||
|
||||
// The baseline fullness: how far a vertex goes out (amount, model units;
|
||||
// negative = in) from the cheeks' shape alone, at rest. Each cheek's
|
||||
// region weight (seam ramp included, so it fades at her midline; the
|
||||
// cleft is left alone) times the amount, along the direction from the
|
||||
// region's centre through the vertex in the region's own (radius-
|
||||
// normalised) space, biased toward her back (+y) so a vertex at the
|
||||
// centre goes straight back. Adds to `delta`.
|
||||
void rear_inflate(const RearShape& s, const RearWeights& w, const float p[3], float amount,
|
||||
float delta[3]);
|
||||
|
||||
// The absolute most a point may be moved from where it rests: a vector's
|
||||
// length becomes limit x tanh(length / limit) -- one to one when small,
|
||||
// easing in smoothly, never reaching the limit -- so no push or pull,
|
||||
// whatever the gain, goes beyond it and none stops dead at it. A
|
||||
// limit <= 0 leaves it unlimited.
|
||||
void soft_limit_vector(float v[3], float limit);
|
||||
|
||||
// A short, rounded to nearest and clamped to int16.
|
||||
short round_short(float v);
|
||||
|
||||
// --- fitting the cheeks to her mesh --------------------------------------
|
||||
|
||||
// What the fit found, in pelvis-local bind units. peak: the rearmost
|
||||
// part of each cheek (mean of its few rearmost points); cap_lo / cap_hi:
|
||||
// the x and z extent of the skin within kCapDepth of that peak; centre /
|
||||
// radius: the region the defaults put there. fallback: too few points
|
||||
// (or a peak outside where a cheek can be), so the nominal shape is used.
|
||||
struct RearFit
|
||||
{
|
||||
bool ok = false;
|
||||
bool fallback = false;
|
||||
int samples[2] = { 0, 0 }; // points considered per side
|
||||
float peak[2][3] = {};
|
||||
float cap_lo[2][2] = {}; // x, z (x signed, as the mesh has it)
|
||||
float cap_hi[2][2] = {};
|
||||
float centre[2][3] = {};
|
||||
float radius[2][3] = {};
|
||||
float seam_x = 0.0f;
|
||||
};
|
||||
|
||||
// Points: pelvis-local bind positions (x, y, z triples), any vertices
|
||||
// of her hips and thighs; metre: model units per metre. Looks at the
|
||||
// back (+y) skin below the belt and above the thigh (z -0.20 .. -0.02 m,
|
||||
// |x| 0.015 .. 0.17 m) on each side of x = 0. The region is centred on
|
||||
// the cap's middle in x and z and 0.42 of its depth radius inside the
|
||||
// peak (so the peak is at full weight with the default core), with
|
||||
// radii 1.5 x the cap's half extents (clamped), depth radius 9 cm.
|
||||
// Always fills `out` (the nominal shape when the mesh does not fit).
|
||||
bool rear_fit(const float* points, int count, float metre, RearFit& out);
|
||||
|
||||
// The sliders, relative to the fitted shape: spread_cm moves each cheek
|
||||
// out (+) / in (-) from its fitted place by (spread_cm - 8) cm (8, the
|
||||
// default, = as fitted); lift_cm up / down; size x (width, 1, height)
|
||||
// scales the radii; core and seam_cm as they are; cleft band of half
|
||||
// width cleft_width_cm, pushed in by cleft_cm (rear_deform's cleft
|
||||
// offset comes from cleft_offset_y).
|
||||
struct RearAdjust
|
||||
{
|
||||
float size = 1.0f;
|
||||
float width = 1.0f;
|
||||
float height = 1.0f;
|
||||
float spread_cm = 8.0f;
|
||||
float lift_cm = 0.0f;
|
||||
float core = 0.5f;
|
||||
float seam_cm = 1.0f;
|
||||
float cleft_width_cm = 2.0f;
|
||||
};
|
||||
const float kSpreadDefaultCm = 8.0f;
|
||||
|
||||
void rear_shape_from_fit(const RearFit& fit, const RearAdjust& adj, float metre, RearShape& out);
|
||||
|
||||
// God mode: cheek `side`'s touch zone, an ellipsoid round its fitted
|
||||
// region with its back edge 1 cm outside the peak and 2 cm beyond the
|
||||
// cap at the sides, moved with spread/lift and grown by touch_cm.
|
||||
void rear_touch_zone(const RearFit& fit, const RearAdjust& adj, int side, float metre,
|
||||
float touch_cm, float centre[3], float semi[3]);
|
||||
|
||||
// Is p inside the axis-aligned ellipsoid (centre, semi)? depth: first-
|
||||
// order distance to its surface; normal: outward, unit (the gradient).
|
||||
// margin grows every semi-axis.
|
||||
bool ellipsoid_contact(const float p[3], const float centre[3], const float semi[3],
|
||||
float margin, float* depth, float normal[3]);
|
||||
|
||||
// --- the pelvis frame --------------------------------------------------
|
||||
|
||||
// m: the pelvis bone's world matrix (row-vector 4x4, translation in
|
||||
// [12..14]); a uniform scale divides out. A world direction / point into
|
||||
// pelvis-local, and a pelvis-local point / direction into the world.
|
||||
// The *_to_local ones return false on a degenerate matrix.
|
||||
bool world_dir_to_local(const float m[16], const float v[3], float out[3]);
|
||||
bool world_point_to_local(const float m[16], const float p[3], float out[3]);
|
||||
void local_point_to_world(const float m[16], const float p[3], float out[3]);
|
||||
void local_dir_to_world(const float m[16], const float v[3], float out[3]);
|
||||
// The mean length of its rows (its scale); 0 when degenerate.
|
||||
float matrix_scale(const float m[16]);
|
||||
|
||||
// A cheek's shown offset in pelvis-local units from the spring's lag
|
||||
// (already pelvis-local): each axis x its gain, the gains eased toward 1
|
||||
// by `boost` (a hand on it), motion into her body (-y) also x inward,
|
||||
// then test_push added along +y (back).
|
||||
void cheek_offset(const float lag_local[3], const float gain[3], float boost, float inward,
|
||||
float test_push, float out[3]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "soft_body.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace rear
|
||||
{
|
||||
bool soft_contact(RearContact& spring, SoftContactState& st,
|
||||
const SoftContactInput& in)
|
||||
{
|
||||
const int hand = in.hand ? 1 : 0;
|
||||
const bool first_touch = in.touching && !st.touching_last;
|
||||
if (!in.gripping)
|
||||
st.grabbing = false;
|
||||
if (!in.touching && !st.grabbing)
|
||||
st.holding = false;
|
||||
else if (in.touching && !st.holding)
|
||||
{
|
||||
st.holding = true;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
st.entry[k] = in.point[k] - in.root[k];
|
||||
// Arriving: the hand's velocity relative to her goes into the
|
||||
// spring, at most 2.5 m/s worth, x 0.6, and only when it moves
|
||||
// into the surface faster than 0.2 m/s and the last point
|
||||
// sample was under 100 ms ago (not a hand lost and found again).
|
||||
const double gap = in.now_seconds - st.last_time;
|
||||
if (!st.touching_last && st.have_last && gap > 0.0 && gap < 0.1 &&
|
||||
in.velocity_valid)
|
||||
{
|
||||
float speed2 = 0.0f, into = 0.0f;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
{
|
||||
speed2 += in.hand_velocity[k] * in.hand_velocity[k];
|
||||
into -= in.hand_velocity[k] * in.normal[k];
|
||||
}
|
||||
const float cap = 2.5f * in.metre;
|
||||
const float scale = speed2 > cap * cap ? cap / std::sqrt(speed2) : 1.0f;
|
||||
if (into > 0.2f * in.metre && std::isfinite(speed2))
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
spring.kick[k] += in.hand_velocity[k] * scale * 0.6f;
|
||||
spring.kick_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (st.holding && in.gripping)
|
||||
st.grabbing = true;
|
||||
if (st.holding && in.point_valid)
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
spring.press[hand][k] = in.point[k] - in.root[k] - st.entry[k];
|
||||
spring.press_time[hand] = in.now_seconds;
|
||||
if (st.grabbing)
|
||||
spring.grab_time[hand] = in.now_seconds;
|
||||
}
|
||||
st.touching_last = in.touching;
|
||||
if (in.point_valid)
|
||||
{
|
||||
for (int k = 0; k < 3; ++k)
|
||||
st.last_point[k] = in.point[k];
|
||||
st.last_time = in.now_seconds;
|
||||
st.have_last = true;
|
||||
}
|
||||
return first_touch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
// Hand contact on a soft part of Lara's body, ported from the Underworld mod
|
||||
// for the rear touch: which way a hand has pressed a part since it took
|
||||
// hold, whether it grips, and the kick of an arriving hand. Pure maths -- no
|
||||
// config(), no game memory, no Windows calls -- so mathtest links it.
|
||||
//
|
||||
// Legend already has its own SoftSpring inside camera_head.cpp, so these
|
||||
// types live in trlvr::rear and the contact record is called RearContact.
|
||||
// The spring itself is not here: rear_jiggle.h's jiggle_step takes what
|
||||
// soft_contact gathers (press -> JiggleContact::shift, kick -> ::kick).
|
||||
|
||||
#include "../vr_math.h"
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace rear
|
||||
{
|
||||
// What soft_contact fills in for one soft part; the caller reads it each
|
||||
// frame. Times are the caller's clock in seconds; 0 means never.
|
||||
struct RearContact
|
||||
{
|
||||
float press[2][3] = {}; // per hand: how far its point has moved
|
||||
// since it took hold, root-relative
|
||||
double press_time[2] = { 0.0, 0.0 };
|
||||
double grab_time[2] = { 0.0, 0.0 }; // last frame a grip held it
|
||||
float kick[3] = {}; // velocity impulse waiting to be taken
|
||||
bool kick_pending = false; // the caller clears it once it has used kick
|
||||
};
|
||||
|
||||
// What soft_contact keeps per hand AND per soft part (one of these for
|
||||
// each pairing; a hand holds only one part at a time).
|
||||
struct SoftContactState
|
||||
{
|
||||
bool holding = false; // this hand has taken hold of the part
|
||||
bool grabbing = false; // ... and its grip is closed on it
|
||||
bool touching_last = false; // inside the body last frame
|
||||
bool have_last = false;
|
||||
float entry[3] = {}; // hand point at first contact, root-relative
|
||||
float last_point[3] = {};
|
||||
double last_time = 0.0;
|
||||
};
|
||||
|
||||
// One hand, one frame. point: the finger/palm point doing the pressing
|
||||
// (the one nearest the part); root: Lara's root; hand_velocity: that
|
||||
// point's velocity relative to her, units/s; normal: the surface normal
|
||||
// at the contact, outward (from BodyHit). The hand that is carrying her
|
||||
// is not fed.
|
||||
struct SoftContactInput
|
||||
{
|
||||
bool touching = false; // point is inside her body this frame
|
||||
bool point_valid = true; // point is fresh (seen < 250 ms ago)
|
||||
bool velocity_valid = true; // hand_velocity is real
|
||||
int hand = 0; // 0 or 1
|
||||
float point[3] = {};
|
||||
float root[3] = {};
|
||||
float hand_velocity[3] = {};
|
||||
float normal[3] = {};
|
||||
bool gripping = false;
|
||||
float metre = 291.0f;
|
||||
double now_seconds = 0.0;
|
||||
};
|
||||
|
||||
// Presses, kicks and grips on one soft part. Call once per frame per
|
||||
// hand per part: `touching` for the part the hand is in (or the nearest
|
||||
// within 8 cm), touching = false for the others so their state
|
||||
// releases. A held grip keeps hold even with the hand outside her body,
|
||||
// until the grip opens. While a hand holds one part (state.holding) do
|
||||
// not pick another for it. Returns true on the frame the hand first
|
||||
// touches (for a haptic pulse). An arriving hand's velocity into the
|
||||
// surface (over 0.2 m/s, last sample under 100 ms old) becomes a kick:
|
||||
// 0.6 x its velocity, at most 2.5 m/s worth.
|
||||
bool soft_contact(RearContact& contact, SoftContactState& hand_state,
|
||||
const SoftContactInput& in);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "tr7_menu_hook.h"
|
||||
|
||||
#include "../common/log.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const uintptr_t kGameTracker = 0x010E5370;
|
||||
const uintptr_t kTrackerPlayer = 0x0C; // Instance*
|
||||
const uintptr_t kTrackerTimeDilation = 0x13C; // float
|
||||
const uintptr_t kInstanceData = 0x1C0; // PlayerProp*
|
||||
const uintptr_t kPropHealthInitial = 0xA4; // float
|
||||
// IncrHealth(float): adds to PlayerData+0x5A0 (disassembled).
|
||||
const uintptr_t kIncrHealth = 0x00576CA0;
|
||||
// PLAYER_DebugSwitchPlayerCharacter (our remap agrees).
|
||||
const uintptr_t kSwitchPlayerCharacter = 0x005A40B0;
|
||||
const uintptr_t kFlags[Tr7FlagCount] = {
|
||||
0x01075BD4, // wireframe
|
||||
0x00CDC9A0, // hide UI
|
||||
0x0107696C, // debug keypad
|
||||
};
|
||||
const char* const kKey = "SOFTWARE\\TRAE-Menu-Hook";
|
||||
bool g_want_health = false;
|
||||
bool g_want_outfit = false;
|
||||
|
||||
bool read(const char* name, void* out, DWORD size)
|
||||
{
|
||||
HKEY key = nullptr;
|
||||
if (RegOpenKeyExA(HKEY_CURRENT_USER, kKey, 0, KEY_READ, &key) !=
|
||||
ERROR_SUCCESS)
|
||||
return false;
|
||||
DWORD got = size;
|
||||
const bool ok = RegQueryValueExA(key, name, nullptr, nullptr,
|
||||
(LPBYTE)out, &got) == ERROR_SUCCESS &&
|
||||
got == size;
|
||||
RegCloseKey(key);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// As TR7-Menu-Hook saves them: REG_BINARY of the value's bytes.
|
||||
void write(const char* name, const void* value, DWORD size)
|
||||
{
|
||||
HKEY key = nullptr;
|
||||
if (RegCreateKeyExA(HKEY_CURRENT_USER, kKey, 0, nullptr, 0,
|
||||
KEY_WRITE, nullptr, &key, nullptr) != ERROR_SUCCESS)
|
||||
return;
|
||||
RegSetValueExA(key, name, 0, REG_BINARY, (const BYTE*)value, size);
|
||||
RegCloseKey(key);
|
||||
log("tr7: saved %s for TR7-Menu-Hook (applies when the game next "
|
||||
"starts)", name);
|
||||
}
|
||||
}
|
||||
|
||||
bool tr7_menu_hook_present()
|
||||
{
|
||||
return GetModuleHandleW(L"TR7-Menu-Hook.asi") != nullptr;
|
||||
}
|
||||
|
||||
void tr7_refill_health() { g_want_health = true; }
|
||||
void tr7_next_outfit() { g_want_outfit = true; }
|
||||
|
||||
namespace
|
||||
{
|
||||
void do_refill_health()
|
||||
{
|
||||
__try
|
||||
{
|
||||
const uintptr_t player =
|
||||
*reinterpret_cast<uintptr_t*>(kGameTracker + kTrackerPlayer);
|
||||
if (!player)
|
||||
return;
|
||||
const uintptr_t prop =
|
||||
*reinterpret_cast<uintptr_t*>(player + kInstanceData);
|
||||
if (!prop)
|
||||
return;
|
||||
const float full =
|
||||
*reinterpret_cast<float*>(prop + kPropHealthInitial);
|
||||
reinterpret_cast<void(__cdecl*)(float)>(kIncrHealth)(full);
|
||||
log("tr7: health refilled (+%.0f)", full);
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
log("tr7: refilling health faulted");
|
||||
}
|
||||
}
|
||||
|
||||
void do_next_outfit()
|
||||
{
|
||||
__try
|
||||
{
|
||||
reinterpret_cast<void(__cdecl*)()>(kSwitchPlayerCharacter)();
|
||||
log("tr7: switched to the next outfit");
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
log("tr7: switching outfit faulted");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void tr7_menu_hook_frame()
|
||||
{
|
||||
// Cleared first: a Present the action itself causes comes back here.
|
||||
if (g_want_health)
|
||||
{
|
||||
g_want_health = false;
|
||||
do_refill_health();
|
||||
}
|
||||
if (g_want_outfit)
|
||||
{
|
||||
g_want_outfit = false;
|
||||
do_next_outfit();
|
||||
}
|
||||
}
|
||||
|
||||
float* tr7_time_multiplier()
|
||||
{
|
||||
return reinterpret_cast<float*>(kGameTracker + kTrackerTimeDilation);
|
||||
}
|
||||
|
||||
bool tr7_flag(Tr7Flag flag, bool* value)
|
||||
{
|
||||
if (flag < 0 || flag >= Tr7FlagCount || !value)
|
||||
return false;
|
||||
__try
|
||||
{
|
||||
*value = *reinterpret_cast<const bool*>(kFlags[flag]);
|
||||
return true;
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void tr7_set_flag(Tr7Flag flag, bool value)
|
||||
{
|
||||
if (flag < 0 || flag >= Tr7FlagCount)
|
||||
return;
|
||||
__try
|
||||
{
|
||||
*reinterpret_cast<bool*>(kFlags[flag]) = value;
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
int tr7_option_int(const char* name, int fallback)
|
||||
{
|
||||
int v = fallback;
|
||||
return read(name, &v, sizeof(v)) ? v : fallback;
|
||||
}
|
||||
void tr7_set_option_int(const char* name, int value)
|
||||
{ write(name, &value, sizeof(value)); }
|
||||
|
||||
bool tr7_option_bool(const char* name, bool fallback)
|
||||
{
|
||||
bool v = fallback;
|
||||
return read(name, &v, sizeof(v)) ? v : fallback;
|
||||
}
|
||||
void tr7_set_option_bool(const char* name, bool value)
|
||||
{ write(name, &value, sizeof(value)); }
|
||||
|
||||
float tr7_option_float(const char* name, float fallback)
|
||||
{
|
||||
float v = fallback;
|
||||
return read(name, &v, sizeof(v)) ? v : fallback;
|
||||
}
|
||||
void tr7_set_option_float(const char* name, float value)
|
||||
{ write(name, &value, sizeof(value)); }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
// TR7-Menu-Hook (another mod, its menu on F8) folded into the settings
|
||||
// overlay (1.1.0, user 2026-10-27: its options inside our panel, not its
|
||||
// window). Its player / time / display items are the game's own functions
|
||||
// and globals (the addresses TR7-Menu-Hook uses for this build, checked
|
||||
// against our symbol remap); its saved options are its registry values,
|
||||
// which it reads when the game starts.
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
bool tr7_menu_hook_present();
|
||||
|
||||
// Requested from the overlay, done by tr7_menu_hook_frame at the start
|
||||
// of the next Present -- not while the overlay's ImGui frame is being
|
||||
// built (user 2026-10-27: Next outfit crashed: the switch loads a model,
|
||||
// the game presents during the load, and that re-entered the overlay's
|
||||
// frame, so the widget after the button found no window).
|
||||
void tr7_refill_health();
|
||||
void tr7_next_outfit();
|
||||
void tr7_menu_hook_frame();
|
||||
// Null when the game's tracker is not there.
|
||||
float* tr7_time_multiplier();
|
||||
// The game's own switches TR7-Menu-Hook shows.
|
||||
enum Tr7Flag { Tr7Wireframe, Tr7HideUi, Tr7DebugKeypad, Tr7FlagCount };
|
||||
bool tr7_flag(Tr7Flag flag, bool* value);
|
||||
void tr7_set_flag(Tr7Flag flag, bool value);
|
||||
|
||||
// Its saved options (HKCU\SOFTWARE\TRAE-Menu-Hook), with its defaults
|
||||
// when unset.
|
||||
int tr7_option_int(const char* name, int fallback);
|
||||
void tr7_set_option_int(const char* name, int value);
|
||||
bool tr7_option_bool(const char* name, bool fallback);
|
||||
void tr7_set_option_bool(const char* name, bool value);
|
||||
float tr7_option_float(const char* name, float fallback);
|
||||
void tr7_set_option_float(const char* name, float value);
|
||||
}
|
||||
+14
-4
@@ -230,9 +230,12 @@ namespace trlvr
|
||||
const bool outer_hud = g_drawing_hud;
|
||||
const bool outer_world_locked = g_world_locked_hud;
|
||||
g_drawing_hud = hud || prompt;
|
||||
g_world_locked_hud = hud && config().menu_world_locked &&
|
||||
menu_screen_active() &&
|
||||
!pause_screen_active();
|
||||
// Only HUDDrawables under an open menu: the gameplay HUD draws
|
||||
// with no menu on the stack. The pause menu has its own setting.
|
||||
g_world_locked_hud = hud && menu_screen_active() &&
|
||||
(pause_screen_active()
|
||||
? config().pause_world_locked
|
||||
: config().menu_world_locked);
|
||||
const int previous_hand = camera_first_person_gpu_hand_draw(
|
||||
camera_first_person_drawable_hand(self));
|
||||
if (have_fields)
|
||||
@@ -262,7 +265,14 @@ namespace trlvr
|
||||
have_fields &&
|
||||
(world_marker_page(texture_page) ||
|
||||
sprite_type >= 0);
|
||||
g_original(self, pass, previous);
|
||||
// hide_gameplay_hud: the head-locked HUD is not drawn at all.
|
||||
// World markers (sprite types), menus and the binocular view
|
||||
// keep drawing.
|
||||
const bool hidden = config().hide_gameplay_hud && hud &&
|
||||
sprite_type < 0 && !menu_screen_active() &&
|
||||
!hud_binocular_view_active();
|
||||
if (!hidden)
|
||||
g_original(self, pass, previous);
|
||||
g_drawing_world_marker = outer_marker;
|
||||
hud_capture_set_drawable(0, 0, -1);
|
||||
camera_first_person_gpu_hand_draw_end(previous_hand);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
// The two VR backends behind xr_runtime.h (1.1.0): OpenXR (xr_runtime.cpp,
|
||||
// oxr_*) and OpenVR / SteamVR (ovr_runtime.cpp, ovr_*). vr_runtime.cpp picks
|
||||
// one at start-up ([vr] vr_runtime) and forwards every xr_* call to it.
|
||||
|
||||
#include "xr_runtime.h"
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
enum VrBackend { VrBackendNone, VrBackendOpenXr, VrBackendOpenVr };
|
||||
VrBackend vr_backend();
|
||||
|
||||
// OpenXR
|
||||
bool oxr_instance_init();
|
||||
void oxr_instance_shutdown();
|
||||
bool oxr_instance_ready();
|
||||
const char* oxr_last_error();
|
||||
const char* oxr_system_name();
|
||||
bool oxr_recommended_size(unsigned* width, unsigned* height);
|
||||
bool oxr_session_create(const XrVulkanHandles& vk);
|
||||
void oxr_session_destroy();
|
||||
bool oxr_session_exists();
|
||||
bool oxr_frame_wait(XrFrame* out);
|
||||
bool oxr_frame_begun();
|
||||
bool oxr_frame_should_render();
|
||||
double oxr_frame_wait_ms();
|
||||
bool oxr_swapchain_matches(unsigned width, unsigned height);
|
||||
bool oxr_swapchain_create(unsigned width, unsigned height);
|
||||
void oxr_swapchain_destroy();
|
||||
int oxr_swapchain_format();
|
||||
bool oxr_swapchain_acquire(uint64_t* vk_image);
|
||||
void oxr_swapchain_release();
|
||||
bool oxr_frame_end(const XrRect* left, const XrRect* right);
|
||||
bool oxr_input_active();
|
||||
bool oxr_input_button(int hand, XrButton button);
|
||||
float oxr_input_trigger(int hand);
|
||||
float oxr_input_squeeze(int hand);
|
||||
void oxr_input_stick(int hand, float* x, float* y);
|
||||
bool oxr_input_haptic(int hand, float seconds, float frequency, float amplitude);
|
||||
XrControllerKind oxr_input_controller(int hand);
|
||||
const char* oxr_input_profile(int hand);
|
||||
|
||||
// OpenVR
|
||||
bool ovr_instance_init();
|
||||
void ovr_instance_shutdown();
|
||||
bool ovr_instance_ready();
|
||||
const char* ovr_last_error();
|
||||
const char* ovr_system_name();
|
||||
bool ovr_recommended_size(unsigned* width, unsigned* height);
|
||||
bool ovr_session_create(const XrVulkanHandles& vk);
|
||||
void ovr_session_destroy();
|
||||
bool ovr_session_exists();
|
||||
bool ovr_frame_wait(XrFrame* out);
|
||||
bool ovr_frame_begun();
|
||||
bool ovr_frame_should_render();
|
||||
double ovr_frame_wait_ms();
|
||||
bool ovr_swapchain_matches(unsigned width, unsigned height);
|
||||
bool ovr_swapchain_create(unsigned width, unsigned height);
|
||||
void ovr_swapchain_destroy();
|
||||
int ovr_swapchain_format();
|
||||
bool ovr_swapchain_acquire(uint64_t* vk_image);
|
||||
void ovr_swapchain_release();
|
||||
bool ovr_frame_end(const XrRect* left, const XrRect* right);
|
||||
bool ovr_input_active();
|
||||
bool ovr_input_button(int hand, XrButton button);
|
||||
float ovr_input_trigger(int hand);
|
||||
float ovr_input_squeeze(int hand);
|
||||
void ovr_input_stick(int hand, float* x, float* y);
|
||||
bool ovr_input_haptic(int hand, float seconds, float frequency, float amplitude);
|
||||
XrControllerKind ovr_input_controller(int hand);
|
||||
const char* ovr_input_profile(int hand);
|
||||
|
||||
// OpenVR has no swapchain: the frame goes to the compositor in place.
|
||||
// One Vulkan image (DXVK's description of the surface) and each eye's
|
||||
// part of it as u0, v0, u1, v1.
|
||||
struct OvrImage
|
||||
{
|
||||
uint64_t image;
|
||||
void* instance;
|
||||
void* physical_device;
|
||||
void* device;
|
||||
void* queue;
|
||||
uint32_t queue_family;
|
||||
uint32_t width, height, format, samples;
|
||||
};
|
||||
bool ovr_submit(const OvrImage& left, const float left_bounds[4],
|
||||
const OvrImage& right, const float right_bounds[4]);
|
||||
}
|
||||
+834
-440
File diff suppressed because it is too large.
Load diff
+24
-10
@@ -3,9 +3,9 @@
|
||||
// The VR controllers, as the keyboard and mouse the game is played with.
|
||||
//
|
||||
// Actions are named for what they do in the game and each presses a fixed
|
||||
// key; which button produces which action lives in input/bindings_*.json and
|
||||
// SteamVR's own bindings UI. See vr_input.cpp for why this is keyboard and
|
||||
// mouse rather than a gamepad.
|
||||
// key; which button produces which action is suggested per controller profile
|
||||
// in xr_runtime.cpp (the runtime may be rebound in its own UI). See vr_input.cpp
|
||||
// for why this is keyboard and mouse rather than a gamepad.
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
@@ -63,6 +63,11 @@ namespace trlvr
|
||||
void vr_input_game_motors(float low, float high);
|
||||
// The grip button of one controller, as last sampled.
|
||||
bool vr_input_grip_held(bool left);
|
||||
// God mode's grab for one physical hand (0 left, 1 right): with
|
||||
// [vr] god_grab_pose and finger data, a pinch (trigger + thumb down) or
|
||||
// a fist (trigger + grip) from the player's fingers (finger_grabbing);
|
||||
// otherwise the grip button.
|
||||
bool vr_input_hand_grab(int hand);
|
||||
// SteamVR skeletal input (finger tracking): each finger's curl, 0
|
||||
// straight .. 1 curled (thumb, index, middle, ring, pinky), and the
|
||||
// splay between neighbours (0 touching .. 1 apart). False when that
|
||||
@@ -94,17 +99,26 @@ namespace trlvr
|
||||
// flashlight. The centre is a handle that moves the whole cross.
|
||||
// Third-person scale / zoom sliders on the off hand's upturned palm
|
||||
// (2026-10-10). Body-frame metres (right, up, forward), as
|
||||
// vr_body_point_head_position takes them. Slider 0 is scale, 1 zoom.
|
||||
// vr_body_point_head_position takes them. Slider 0 is scale, 1 zoom,
|
||||
// 2 side.
|
||||
struct VrTpSliders
|
||||
{
|
||||
bool visible = false;
|
||||
float opacity = 0.0f; // 0..1, fading in
|
||||
float start[2][3]{}; // each bar's ends
|
||||
float end[2][3]{};
|
||||
float knob[2][3]{}; // each knob's centre
|
||||
bool hover[2] = { false, false };
|
||||
bool held[2] = { false, false };
|
||||
float icon[2][3]{}; // where each slider's icon sits
|
||||
float start[3][3]{}; // each bar's ends
|
||||
float end[3][3]{};
|
||||
float knob[3][3]{}; // each knob's centre
|
||||
bool hover[3] = { false, false, false };
|
||||
bool held[3] = { false, false, false };
|
||||
float icon[3][3]{}; // where each slider's icon sits
|
||||
int notches[3] = { 0, 0, 0 }; // sticking points per bar
|
||||
float notch[3][2]{}; // their body-frame x
|
||||
// [developer] slider_debug = 1: the grab sphere round each knob
|
||||
// and the other hand's two test points (0 palm, 1 index tip).
|
||||
bool debug = false;
|
||||
float reach = 0.0f; // metres
|
||||
bool probe_ok[2] = { false, false };
|
||||
float probe[2][3]{};
|
||||
};
|
||||
bool vr_input_tp_sliders(VrTpSliders* out);
|
||||
|
||||
|
||||
@@ -252,6 +252,68 @@ namespace trlvr
|
||||
return result;
|
||||
}
|
||||
|
||||
Mat4 rotation_from_vector(const float rv3[3])
|
||||
{
|
||||
Mat4 result = Mat4::identity();
|
||||
const float theta = sqrtf(rv3[0]*rv3[0] + rv3[1]*rv3[1] + rv3[2]*rv3[2]);
|
||||
if (!std::isfinite(theta) || theta < 1.0e-9f)
|
||||
return result;
|
||||
const float k[3] = { rv3[0] / theta, rv3[1] / theta, rv3[2] / theta };
|
||||
const float s = sinf(theta), c = cosf(theta);
|
||||
// Column-vector Rodrigues R, stored transposed for row vectors.
|
||||
const float cross[3][3] = {
|
||||
{ 0.0f, -k[2], k[1] }, { k[2], 0.0f, -k[0] }, { -k[1], k[0], 0.0f } };
|
||||
for (int a = 0; a < 3; ++a)
|
||||
for (int b = 0; b < 3; ++b)
|
||||
{
|
||||
const float r = (a == b ? c : 0.0f) + (1.0f - c) * k[a] * k[b] +
|
||||
s * cross[a][b];
|
||||
result.m[b][a] = r;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool rotation_to_vector(const Mat4& m, float rv3[3])
|
||||
{
|
||||
for (int r = 0; r < 3; ++r)
|
||||
for (int c = 0; c < 3; ++c)
|
||||
if (!std::isfinite(m.m[r][c]))
|
||||
return false;
|
||||
// Column-form R[a][b] = m[b][a].
|
||||
const float R[3][3] = {
|
||||
{ m.m[0][0], m.m[1][0], m.m[2][0] },
|
||||
{ m.m[0][1], m.m[1][1], m.m[2][1] },
|
||||
{ m.m[0][2], m.m[1][2], m.m[2][2] } };
|
||||
const float s[3] = { 0.5f * (R[2][1] - R[1][2]), 0.5f * (R[0][2] - R[2][0]),
|
||||
0.5f * (R[1][0] - R[0][1]) };
|
||||
const float c = 0.5f * (R[0][0] + R[1][1] + R[2][2] - 1.0f);
|
||||
const float sl = sqrtf(s[0]*s[0] + s[1]*s[1] + s[2]*s[2]);
|
||||
const float theta = atan2f(sl, c);
|
||||
rv3[0] = rv3[1] = rv3[2] = 0.0f;
|
||||
if (sl > 1.0e-6f)
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
rv3[i] = s[i] / sl * theta;
|
||||
return true;
|
||||
}
|
||||
if (c > 0.0f)
|
||||
return true; // no rotation
|
||||
// Half a turn: the axis from the diagonal (sign of the largest part
|
||||
// is arbitrary).
|
||||
int a = 0;
|
||||
if (R[1][1] > R[a][a]) a = 1;
|
||||
if (R[2][2] > R[a][a]) a = 2;
|
||||
const float ka = sqrtf(fmaxf(0.0f, (R[a][a] - c) / (1.0f - c)));
|
||||
if (ka < 1.0e-6f)
|
||||
return true;
|
||||
float k[3];
|
||||
for (int b = 0; b < 3; ++b)
|
||||
k[b] = b == a ? ka : (R[a][b] + R[b][a]) / (2.0f * (1.0f - c) * ka);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
rv3[i] = k[i] * theta;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool solve_two_bone_arm(const float* shoulder3, const float* elbow3,
|
||||
const float* wrist3, const float* target3,
|
||||
float* solved_elbow3, float* solved_wrist3)
|
||||
|
||||
@@ -80,6 +80,17 @@ namespace trlvr
|
||||
// first-person arm solver; returns identity for a degenerate direction.
|
||||
Mat4 rotation_between(const float* from3, const float* to3);
|
||||
|
||||
// Rigid row-vector rotation about the axis rv/|rv| by |rv| radians, turning
|
||||
// counter-clockwise looking down the axis toward the origin (right-hand
|
||||
// rule: rv = (0,0,pi/2) takes +x to +y, as rotation_between does). Identity
|
||||
// for a zero or non-finite vector.
|
||||
Mat4 rotation_from_vector(const float rv3[3]);
|
||||
|
||||
// The inverse: the rotation vector (axis * angle, angle 0..pi) of the upper
|
||||
// 3x3 of `m`, which must be (near) orthonormal. False for a non-finite
|
||||
// matrix.
|
||||
bool rotation_to_vector(const Mat4& m, float rv3[3]);
|
||||
|
||||
// Keep the wrist within the two animated arm lengths, and preserve the
|
||||
// original elbow's bend side. Returns false for a degenerate arm.
|
||||
bool solve_two_bone_arm(const float* shoulder3, const float* elbow3,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
#include "vr_backend.h"
|
||||
|
||||
#include "../common/config.h"
|
||||
#include "../common/log.h"
|
||||
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
// xr_runtime.h is the one VR interface the mod uses. Since 1.1.0 it has two
|
||||
// implementations: OpenXR (xr_runtime.cpp, the default) and OpenVR / SteamVR
|
||||
// (ovr_runtime.cpp, kept as the proven fallback while OpenXR settles in).
|
||||
// [vr] vr_runtime picks: auto tries OpenXR and falls back to OpenVR when
|
||||
// openxr_loader.dll is missing beside the game or no OpenXR runtime starts;
|
||||
// openxr / openvr use only that one.
|
||||
//
|
||||
// openxr_loader.dll is delay-loaded (build.bat), so the game starts without
|
||||
// it: it is loaded here by full path first, and no OpenXR function is called
|
||||
// unless that worked.
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
namespace
|
||||
{
|
||||
VrBackend g_backend = VrBackendNone;
|
||||
char g_error[512] = "no VR runtime started";
|
||||
|
||||
bool openxr_loader_present()
|
||||
{
|
||||
wchar_t path[MAX_PATH]{};
|
||||
swprintf_s(path, L"%sopenxr_loader.dll", exe_dir());
|
||||
if (GetModuleHandleW(L"openxr_loader.dll"))
|
||||
return true;
|
||||
return LoadLibraryExW(path, nullptr,
|
||||
LOAD_WITH_ALTERED_SEARCH_PATH) != nullptr;
|
||||
}
|
||||
|
||||
bool try_openxr()
|
||||
{
|
||||
if (!openxr_loader_present())
|
||||
{
|
||||
sprintf_s(g_error, "openxr_loader.dll not found beside the game");
|
||||
log("vr: OpenXR skipped -- %s", g_error);
|
||||
return false;
|
||||
}
|
||||
if (oxr_instance_init())
|
||||
{
|
||||
g_backend = VrBackendOpenXr;
|
||||
log("vr: using OpenXR (%s)", oxr_system_name());
|
||||
return true;
|
||||
}
|
||||
sprintf_s(g_error, "OpenXR: %s", oxr_last_error());
|
||||
log("vr: OpenXR did not start -- %s", oxr_last_error());
|
||||
oxr_instance_shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool try_openvr()
|
||||
{
|
||||
if (ovr_instance_init())
|
||||
{
|
||||
g_backend = VrBackendOpenVr;
|
||||
log("vr: using OpenVR / SteamVR (%s)", ovr_system_name());
|
||||
return true;
|
||||
}
|
||||
sprintf_s(g_error, "OpenVR: %s", ovr_last_error());
|
||||
log("vr: OpenVR did not start -- %s", ovr_last_error());
|
||||
ovr_instance_shutdown();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
VrBackend vr_backend() { return g_backend; }
|
||||
|
||||
#define VR_FORWARD(call, fallback) \
|
||||
switch (g_backend) \
|
||||
{ \
|
||||
case VrBackendOpenXr: return oxr_##call; \
|
||||
case VrBackendOpenVr: return ovr_##call; \
|
||||
default: return fallback; \
|
||||
}
|
||||
|
||||
bool xr_instance_init()
|
||||
{
|
||||
if (g_backend != VrBackendNone)
|
||||
return true;
|
||||
const int choice = config().vr_runtime;
|
||||
log("vr: runtime choice %s",
|
||||
choice == 1 ? "openxr" : choice == 2 ? "openvr" : "auto");
|
||||
if (choice != 2 && try_openxr())
|
||||
return true;
|
||||
if (choice != 1 && try_openvr())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void xr_instance_shutdown()
|
||||
{
|
||||
if (g_backend == VrBackendOpenXr)
|
||||
oxr_instance_shutdown();
|
||||
else if (g_backend == VrBackendOpenVr)
|
||||
ovr_instance_shutdown();
|
||||
g_backend = VrBackendNone;
|
||||
}
|
||||
|
||||
bool xr_instance_ready() { VR_FORWARD(instance_ready(), false) }
|
||||
const char* xr_last_error() { VR_FORWARD(last_error(), g_error) }
|
||||
const char* xr_system_name() { VR_FORWARD(system_name(), "") }
|
||||
bool xr_recommended_size(unsigned* width, unsigned* height)
|
||||
{ VR_FORWARD(recommended_size(width, height), false) }
|
||||
|
||||
bool xr_session_create(const XrVulkanHandles& vk)
|
||||
{ VR_FORWARD(session_create(vk), false) }
|
||||
void xr_session_destroy()
|
||||
{
|
||||
if (g_backend == VrBackendOpenXr)
|
||||
oxr_session_destroy();
|
||||
else if (g_backend == VrBackendOpenVr)
|
||||
ovr_session_destroy();
|
||||
}
|
||||
bool xr_session_exists() { VR_FORWARD(session_exists(), false) }
|
||||
|
||||
bool xr_frame_wait(XrFrame* out) { VR_FORWARD(frame_wait(out), false) }
|
||||
bool xr_frame_begun() { VR_FORWARD(frame_begun(), false) }
|
||||
bool xr_frame_should_render() { VR_FORWARD(frame_should_render(), false) }
|
||||
double xr_frame_wait_ms() { VR_FORWARD(frame_wait_ms(), 0.0) }
|
||||
|
||||
bool xr_swapchain_matches(unsigned width, unsigned height)
|
||||
{ VR_FORWARD(swapchain_matches(width, height), false) }
|
||||
bool xr_swapchain_create(unsigned width, unsigned height)
|
||||
{ VR_FORWARD(swapchain_create(width, height), false) }
|
||||
void xr_swapchain_destroy()
|
||||
{
|
||||
if (g_backend == VrBackendOpenXr)
|
||||
oxr_swapchain_destroy();
|
||||
else if (g_backend == VrBackendOpenVr)
|
||||
ovr_swapchain_destroy();
|
||||
}
|
||||
int xr_swapchain_format() { VR_FORWARD(swapchain_format(), 0) }
|
||||
bool xr_swapchain_acquire(uint64_t* vk_image)
|
||||
{ VR_FORWARD(swapchain_acquire(vk_image), false) }
|
||||
void xr_swapchain_release()
|
||||
{
|
||||
if (g_backend == VrBackendOpenXr)
|
||||
oxr_swapchain_release();
|
||||
else if (g_backend == VrBackendOpenVr)
|
||||
ovr_swapchain_release();
|
||||
}
|
||||
bool xr_frame_end(const XrRect* left, const XrRect* right)
|
||||
{ VR_FORWARD(frame_end(left, right), false) }
|
||||
|
||||
bool xr_input_active() { VR_FORWARD(input_active(), false) }
|
||||
bool xr_input_button(int hand, XrButton button)
|
||||
{ VR_FORWARD(input_button(hand, button), false) }
|
||||
float xr_input_trigger(int hand) { VR_FORWARD(input_trigger(hand), 0.0f) }
|
||||
float xr_input_squeeze(int hand) { VR_FORWARD(input_squeeze(hand), 0.0f) }
|
||||
void xr_input_stick(int hand, float* x, float* y)
|
||||
{
|
||||
if (g_backend == VrBackendOpenXr)
|
||||
oxr_input_stick(hand, x, y);
|
||||
else if (g_backend == VrBackendOpenVr)
|
||||
ovr_input_stick(hand, x, y);
|
||||
else
|
||||
{
|
||||
if (x) *x = 0.0f;
|
||||
if (y) *y = 0.0f;
|
||||
}
|
||||
}
|
||||
bool xr_input_haptic(int hand, float seconds, float frequency, float amplitude)
|
||||
{ VR_FORWARD(input_haptic(hand, seconds, frequency, amplitude), false) }
|
||||
XrControllerKind xr_input_controller(int hand)
|
||||
{ VR_FORWARD(input_controller(hand), XrControllerNone) }
|
||||
const char* xr_input_profile(int hand) { VR_FORWARD(input_profile(hand), "") }
|
||||
|
||||
#undef VR_FORWARD
|
||||
}
|
||||
+400
-375
File diff suppressed because it is too large.
Load diff
+28
-21
@@ -1,14 +1,16 @@
|
||||
#pragma once
|
||||
// The OpenVR session: headset, eye frusta, poses.
|
||||
// The VR session: headset, eye frusta, poses.
|
||||
//
|
||||
// openvr_api.dll is loaded by hand rather than linked. This DLL is the game's
|
||||
// d3d9.dll, so a missing or broken OpenVR runtime must not stop the game from
|
||||
// starting -- it logs why and the game runs flat.
|
||||
|
||||
// The runtime is OpenXR (xr_runtime.cpp). This DLL is the game's d3d9.dll,
|
||||
// so a missing or broken runtime must not stop the game from starting -- it
|
||||
// logs why and the game runs flat.
|
||||
#include "vr_math.h"
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
struct TrackedPose;
|
||||
struct XrEyeView;
|
||||
|
||||
// EyeCenter is not a real eye: it is the average of the two frusta with no
|
||||
// lateral offset, used while a single image is being sent to both eyes.
|
||||
enum Eye { EyeLeft = 0, EyeRight = 1, EyeCenter = 2 };
|
||||
@@ -23,7 +25,7 @@ namespace trlvr
|
||||
// Offset from the head origin, in metres.
|
||||
float offset_x, offset_y, offset_z;
|
||||
|
||||
// Eye-to-head rotation (OpenVR axes, row-major). Identity on most
|
||||
// Eye-to-head rotation (-Z forward, +Y up; row-major). Identity on most
|
||||
// headsets; canted displays (Pimax and others) turn each eye outward,
|
||||
// and the tangents above are measured about that turned axis.
|
||||
float rot[3][3];
|
||||
@@ -31,18 +33,9 @@ namespace trlvr
|
||||
|
||||
bool vr_init();
|
||||
|
||||
// The compositor, fetched through our own dynamic loader. openvr.h's
|
||||
// vr::VRCompositor() helper cannot be used: it calls the *imported*
|
||||
// VR_GetGenericInterface, and we deliberately do not link openvr_api.
|
||||
// Null unless the session came up as a Scene application.
|
||||
void* vr_compositor_raw();
|
||||
void vr_shutdown();
|
||||
bool vr_ready();
|
||||
|
||||
// Any OpenVR interface by version string, from the session already up --
|
||||
// IVRInput for the controllers, say. Null until vr_ready().
|
||||
void* vr_get_interface(const char* version);
|
||||
|
||||
// Empty until vr_init succeeds.
|
||||
const char* vr_status();
|
||||
|
||||
@@ -62,14 +55,17 @@ namespace trlvr
|
||||
// headset can see gets thrown away.
|
||||
float vr_max_tangent();
|
||||
|
||||
// Refresh the head pose. Cheap; safe to call once a frame. Does nothing
|
||||
// once vr_set_render_poses is supplying the compositor's poses.
|
||||
// Once a frame, at the frame boundary. The poses themselves arrive
|
||||
// through vr_set_render_poses.
|
||||
void vr_update_pose();
|
||||
|
||||
// The poses returned by IVRCompositor::WaitGetPoses (seated space): the
|
||||
// predicted poses the compositor assumes the next frame is rendered
|
||||
// with. `poses` is a vr::TrackedDevicePose_t array.
|
||||
void vr_set_render_poses(const void* poses, unsigned count);
|
||||
// The poses predicted for the frame about to be rendered (LOCAL space),
|
||||
// kPoseCount of them: head, left hand, right hand.
|
||||
void vr_set_render_poses(const TrackedPose* poses, unsigned count);
|
||||
|
||||
// The eye frusta and offsets for that frame. Until the first call the
|
||||
// session reports a provisional symmetric frustum.
|
||||
void vr_set_eye_views(const XrEyeView eyes[2]);
|
||||
|
||||
// Head pose as a view matrix in the engine's row-vector convention,
|
||||
// in game units. Identity until a pose has been read.
|
||||
@@ -152,6 +148,17 @@ namespace trlvr
|
||||
// forward), as used by the tracked-hand grab-zone renderer.
|
||||
bool vr_project_head_point(Eye eye, float x, float y, float z,
|
||||
float* u, float* v);
|
||||
// The headset in the runtime's tracking space (metres, x right, y up,
|
||||
// z back): its position and its level forward (x, z; unit length).
|
||||
// False without a valid head pose or when it looks straight up/down.
|
||||
bool vr_head_tracking_level(float position[3], float* forward_x,
|
||||
float* forward_z);
|
||||
// Project a fixed point in that tracking space into an eye's normalized
|
||||
// texture coordinates, with its depth in front of the eye in metres
|
||||
// (negative or small behind it -- the caller decides; u and v are only
|
||||
// meaningful with a positive depth). False without a head pose.
|
||||
bool vr_project_tracking_point(Eye eye, const float point[3], float* u,
|
||||
float* v, float* depth);
|
||||
|
||||
// Latest tracked controller transform relative to the HMD, in engine
|
||||
// coordinates and units. This is intentionally independent of the game
|
||||
|
||||
+873
-187
File diff suppressed because it is too large.
Load diff
+15
-11
@@ -1,15 +1,10 @@
|
||||
#pragma once
|
||||
// Handing finished frames to the compositor.
|
||||
// Handing finished frames to the headset.
|
||||
//
|
||||
// OpenVR will not take a Direct3D 9 texture. Our DXVK fork exports
|
||||
// Direct3DCreateVR9, which gives back the VkImage and the device/queue handles
|
||||
// behind any D3D9 surface, so the back buffer the game just drew can go
|
||||
// straight to the compositor with no copy.
|
||||
//
|
||||
// This stage submits the same image to both eyes -- mono. It exists to prove
|
||||
// the interop, the Scene session and the compositor timing all work before
|
||||
// stereo depends on them.
|
||||
|
||||
// OpenXR has no Direct3D 9 binding. Our DXVK fork exports Direct3DCreateVR9,
|
||||
// which gives the Vulkan device behind the D3D9 one -- what the OpenXR
|
||||
// session is created on -- and copies the back buffer the game just drew
|
||||
// into the runtime's swapchain image.
|
||||
#include <d3d9.h>
|
||||
|
||||
namespace trlvr
|
||||
@@ -18,6 +13,9 @@ namespace trlvr
|
||||
// its argument straight to DXVK's own device type.
|
||||
bool vr_submit_init(IDirect3DDevice9* real_device);
|
||||
void vr_submit_shutdown();
|
||||
// Before the real device is released: the OpenXR session lives on its
|
||||
// Vulkan device and has to go first.
|
||||
void vr_submit_device_closing(IDirect3DDevice9* real_device);
|
||||
bool vr_submit_ready();
|
||||
|
||||
// Call with the swap chain's back buffer, before presenting it. In stereo
|
||||
@@ -25,7 +23,8 @@ namespace trlvr
|
||||
// submits both; in mono it submits the one image to both eyes.
|
||||
void vr_submit_frame(IDirect3DSurface9* back_buffer);
|
||||
|
||||
// Blocks until the compositor wants the next frame, and refreshes poses.
|
||||
// Blocks until the runtime wants the next frame (xrWaitFrame), begins it
|
||||
// and refreshes the poses.
|
||||
// This is what paces the game to the headset.
|
||||
void vr_submit_wait();
|
||||
// Counts one game draw call for the 5 s performance line.
|
||||
@@ -42,4 +41,9 @@ namespace trlvr
|
||||
// The main hand points at another mod's ImGui menu panel: its trigger
|
||||
// is the menu's mouse button, not the game's.
|
||||
bool vr_submit_imgui_pointing();
|
||||
|
||||
// The gamma ramp the game asks the device for (its brightness and
|
||||
// contrast sliders): a D3DGAMMARAMP. Windows ignores it in a window,
|
||||
// so the submit path applies it to the frame instead.
|
||||
void vr_submit_set_game_ramp(const void* ramp);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,149 @@
|
||||
#pragma once
|
||||
// Everything that talks to OpenXR: instance, session, frame loop, swapchain
|
||||
// and the controller actions. The rest of the mod sees poses as 3x4 matrices
|
||||
// and frusta as tangents, and never includes openxr.h.
|
||||
//
|
||||
// The loader is linked statically, so there is no DLL of ours to go missing;
|
||||
// a missing or broken runtime fails xrCreateInstance, which is logged, and
|
||||
// the game runs flat.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace trlvr
|
||||
{
|
||||
// Device -> tracking space, row-major 3x4: the rotation's columns are the
|
||||
// device's local axes, the last column its position in metres. Right
|
||||
// handed, +Y up, -Z forward.
|
||||
struct Mat34
|
||||
{
|
||||
float m[3][4];
|
||||
};
|
||||
|
||||
struct Vec3
|
||||
{
|
||||
float v[3];
|
||||
};
|
||||
|
||||
struct TrackedPose
|
||||
{
|
||||
Mat34 mDeviceToAbsoluteTracking;
|
||||
Vec3 vVelocity;
|
||||
Vec3 vAngularVelocity;
|
||||
bool bPoseIsValid;
|
||||
bool bDeviceIsConnected;
|
||||
};
|
||||
|
||||
// The poses the mod tracks. The hands are the controllers' grip poses.
|
||||
enum : unsigned { kPoseHmd = 0, kPoseLeft = 1, kPoseRight = 2, kPoseCount = 3 };
|
||||
|
||||
// One eye as the runtime reports it for the frame being rendered.
|
||||
struct XrEyeView
|
||||
{
|
||||
// Half-angle tangents in the layout EyeInfo has always held: left
|
||||
// negative, right positive, and "top" the down edge (negative) with
|
||||
// "bottom" the up edge (positive).
|
||||
float tan_left, tan_right, tan_top, tan_bottom;
|
||||
Mat34 eye_to_head;
|
||||
};
|
||||
|
||||
// --- instance ---------------------------------------------------------
|
||||
// Instance and system only; no graphics yet. Also publishes the Vulkan
|
||||
// extensions the runtime needs, for the DXVK fork to pick up when it
|
||||
// creates its instance and device.
|
||||
bool xr_instance_init();
|
||||
void xr_instance_shutdown();
|
||||
bool xr_instance_ready();
|
||||
const char* xr_last_error();
|
||||
const char* xr_system_name();
|
||||
bool xr_recommended_size(unsigned* width, unsigned* height);
|
||||
|
||||
// --- session ----------------------------------------------------------
|
||||
struct XrVulkanHandles
|
||||
{
|
||||
void* instance;
|
||||
void* physical_device;
|
||||
void* device;
|
||||
uint32_t queue_family;
|
||||
uint32_t queue_index;
|
||||
};
|
||||
|
||||
// The runtime may use the Vulkan queue inside these, so the caller holds
|
||||
// DXVK's submission queue across them.
|
||||
bool xr_session_create(const XrVulkanHandles& vk);
|
||||
void xr_session_destroy();
|
||||
bool xr_session_exists();
|
||||
|
||||
// --- frame ------------------------------------------------------------
|
||||
struct XrFrame
|
||||
{
|
||||
TrackedPose poses[kPoseCount];
|
||||
XrEyeView eyes[2];
|
||||
bool eyes_valid;
|
||||
};
|
||||
|
||||
// Polls events, then xrWaitFrame + xrBeginFrame and the poses predicted
|
||||
// for that frame. False when no frame was begun (session not running).
|
||||
// Does not touch the Vulkan queue.
|
||||
bool xr_frame_wait(XrFrame* out);
|
||||
bool xr_frame_begun();
|
||||
// False while the runtime wants frames kept in step but not drawn.
|
||||
bool xr_frame_should_render();
|
||||
// Milliseconds xrWaitFrame blocked for the frame last begun.
|
||||
double xr_frame_wait_ms();
|
||||
|
||||
// The swapchain is created to fit what is submitted: one image as wide
|
||||
// as both eyes side by side. These use the queue; hold the lock.
|
||||
bool xr_swapchain_matches(unsigned width, unsigned height);
|
||||
bool xr_swapchain_create(unsigned width, unsigned height);
|
||||
void xr_swapchain_destroy();
|
||||
// VkFormat of the swapchain, and whether it is BGRA like the back buffer.
|
||||
int xr_swapchain_format();
|
||||
bool xr_swapchain_acquire(uint64_t* vk_image);
|
||||
void xr_swapchain_release();
|
||||
|
||||
struct XrRect
|
||||
{
|
||||
int x, y, width, height;
|
||||
};
|
||||
// Ends the frame begun by xr_frame_wait with one projection layer, each
|
||||
// eye a rectangle of the swapchain image; null rectangles end it with no
|
||||
// layer. The poses are the ones xr_frame_wait located.
|
||||
bool xr_frame_end(const XrRect* left, const XrRect* right);
|
||||
|
||||
// --- controllers --------------------------------------------------------
|
||||
// Physical inputs, per hand (0 left, 1 right). Which game action each
|
||||
// one produces is decided in vr_input.cpp.
|
||||
enum XrButton
|
||||
{
|
||||
XrButtonStick, // thumbstick click
|
||||
XrButtonPrimary, // A / X (Index: A)
|
||||
XrButtonSecondary, // B / Y (Index: B)
|
||||
XrButtonTrackpad, // Index and WMR
|
||||
XrButtonMenu, // WMR menu, left Touch menu
|
||||
// Capacitive sensors (Touch, Index): a finger resting on them.
|
||||
XrTouchTrigger, // index finger on the trigger
|
||||
XrTouchThumb, // thumb on the stick, a face button or the rest
|
||||
XrButtonCount
|
||||
};
|
||||
|
||||
enum XrControllerKind
|
||||
{
|
||||
XrControllerNone,
|
||||
XrControllerTouch,
|
||||
XrControllerIndex,
|
||||
XrControllerWmr,
|
||||
XrControllerHpReverb,
|
||||
XrControllerSimple,
|
||||
};
|
||||
|
||||
// True once the actions were synced this frame (session focused).
|
||||
bool xr_input_active();
|
||||
bool xr_input_button(int hand, XrButton button);
|
||||
float xr_input_trigger(int hand);
|
||||
float xr_input_squeeze(int hand);
|
||||
void xr_input_stick(int hand, float* x, float* y);
|
||||
bool xr_input_haptic(int hand, float seconds, float frequency, float amplitude);
|
||||
XrControllerKind xr_input_controller(int hand);
|
||||
// The interaction profile path for that hand, "" until one is bound.
|
||||
const char* xr_input_profile(int hand);
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2025 Omar Cornut
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Vendored
+142
@@ -0,0 +1,142 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// DEAR IMGUI COMPILE-TIME OPTIONS
|
||||
// Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure.
|
||||
// You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions.
|
||||
//-----------------------------------------------------------------------------
|
||||
// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it)
|
||||
// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template.
|
||||
//-----------------------------------------------------------------------------
|
||||
// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp
|
||||
// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures.
|
||||
// Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts.
|
||||
// Call IMGUI_CHECKVERSION() from your .cpp file to verify that the data structures your files are using are matching the ones imgui.cpp is using.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
//---- Define assertion handler. Defaults to calling assert().
|
||||
// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement.
|
||||
//#define IM_ASSERT(_EXPR) MyAssert(_EXPR)
|
||||
//#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts
|
||||
|
||||
//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows
|
||||
// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility.
|
||||
// - Windows DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions()
|
||||
// for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details.
|
||||
//#define IMGUI_API __declspec(dllexport) // MSVC Windows: DLL export
|
||||
//#define IMGUI_API __declspec(dllimport) // MSVC Windows: DLL import
|
||||
//#define IMGUI_API __attribute__((visibility("default"))) // GCC/Clang: override visibility when set is hidden
|
||||
|
||||
//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names.
|
||||
//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
|
||||
//---- Disable all of Dear ImGui or don't implement standard windows/tools.
|
||||
// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp.
|
||||
//#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty.
|
||||
//#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty.
|
||||
//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty.
|
||||
|
||||
//---- Don't implement some functions to reduce linkage requirements.
|
||||
//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a)
|
||||
//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW)
|
||||
//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a)
|
||||
//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME).
|
||||
//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default).
|
||||
//#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")).
|
||||
//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf)
|
||||
//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself.
|
||||
//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies)
|
||||
//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function.
|
||||
//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions().
|
||||
//#define IMGUI_DISABLE_DEFAULT_FONT // Disable default embedded font (ProggyClean.ttf), remove ~9.5 KB from output binary. AddFontDefault() will assert.
|
||||
//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available
|
||||
|
||||
//---- Enable Test Engine / Automation features.
|
||||
//#define IMGUI_ENABLE_TEST_ENGINE // Enable imgui_test_engine hooks. Generally set automatically by include "imgui_te_config.h", see Test Engine for details.
|
||||
|
||||
//---- Include imgui_user.h at the end of imgui.h as a convenience
|
||||
// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included.
|
||||
//#define IMGUI_INCLUDE_IMGUI_USER_H
|
||||
//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h"
|
||||
|
||||
//---- Pack vertex colors as BGRA8 instead of RGBA8 (to avoid converting from one to another). Need dedicated backend support.
|
||||
//#define IMGUI_USE_BGRA_PACKED_COLOR
|
||||
|
||||
//---- Use legacy CRC32-adler tables (used before 1.91.6), in order to preserve old .ini data that you cannot afford to invalidate.
|
||||
//#define IMGUI_USE_LEGACY_CRC32_ADLER
|
||||
|
||||
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
|
||||
//#define IMGUI_USE_WCHAR32
|
||||
|
||||
//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
|
||||
// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.
|
||||
//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h"
|
||||
//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h"
|
||||
//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if IMGUI_USE_STB_SPRINTF is defined.
|
||||
//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION
|
||||
//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION
|
||||
//#define IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION // only disabled if IMGUI_USE_STB_SPRINTF is defined.
|
||||
|
||||
//---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined)
|
||||
// Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h.
|
||||
//#define IMGUI_USE_STB_SPRINTF
|
||||
|
||||
//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui)
|
||||
// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided).
|
||||
// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'.
|
||||
//#define IMGUI_ENABLE_FREETYPE
|
||||
|
||||
//---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT)
|
||||
// Only works in combination with IMGUI_ENABLE_FREETYPE.
|
||||
// - lunasvg is currently easier to acquire/install, as e.g. it is part of vcpkg.
|
||||
// - plutosvg will support more fonts and may load them faster. It currently requires to be built manually but it is fairly easy. See misc/freetype/README for instructions.
|
||||
// - Both require headers to be available in the include path + program to be linked with the library code (not provided).
|
||||
// - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement)
|
||||
//#define IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
//#define IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
|
||||
//---- Use stb_truetype to build and rasterize the font atlas (default)
|
||||
// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend.
|
||||
//#define IMGUI_ENABLE_STB_TRUETYPE
|
||||
|
||||
//---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4.
|
||||
// This will be inlined as part of ImVec2 and ImVec4 class declarations.
|
||||
/*
|
||||
#define IM_VEC2_CLASS_EXTRA \
|
||||
constexpr ImVec2(const MyVec2& f) : x(f.x), y(f.y) {} \
|
||||
operator MyVec2() const { return MyVec2(x,y); }
|
||||
|
||||
#define IM_VEC4_CLASS_EXTRA \
|
||||
constexpr ImVec4(const MyVec4& f) : x(f.x), y(f.y), z(f.z), w(f.w) {} \
|
||||
operator MyVec4() const { return MyVec4(x,y,z,w); }
|
||||
*/
|
||||
//---- ...Or use Dear ImGui's own very basic math operators.
|
||||
//#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
|
||||
//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices.
|
||||
// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices).
|
||||
// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer.
|
||||
// Read about ImGuiBackendFlags_RendererHasVtxOffset for details.
|
||||
//#define ImDrawIdx unsigned int
|
||||
|
||||
//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly)
|
||||
//struct ImDrawList;
|
||||
//struct ImDrawCmd;
|
||||
//typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data);
|
||||
//#define ImDrawCallback MyImDrawCallback
|
||||
|
||||
//---- Debug Tools: Macro to break in Debugger (we provide a default implementation of this in the codebase)
|
||||
// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.)
|
||||
//#define IM_DEBUG_BREAK IM_ASSERT(0)
|
||||
//#define IM_DEBUG_BREAK __debugbreak()
|
||||
|
||||
//---- Debug Tools: Enable slower asserts
|
||||
//#define IMGUI_DEBUG_PARANOID
|
||||
|
||||
//---- Tip: You can add extra functions within the ImGui:: namespace from anywhere (e.g. your own sources/header files)
|
||||
/*
|
||||
namespace ImGui
|
||||
{
|
||||
void MyFunction(const char* name, MyMatrix44* mtx);
|
||||
}
|
||||
*/
|
||||
Vendored
+16965
File diff suppressed because it is too large.
Load diff
Vendored
+3745
File diff suppressed because it is too large.
Load diff
Vendored
+4840
File diff suppressed because it is too large.
Load diff
Vendored
+424
@@ -0,0 +1,424 @@
|
||||
// dear imgui: Renderer Backend for DirectX9
|
||||
// This needs to be used along with a Platform Backend (e.g. Win32)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'LPDIRECT3DTEXTURE9' as ImTextureID. Read the FAQ about ImTextureID!
|
||||
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
|
||||
// [X] Renderer: IMGUI_USE_BGRA_PACKED_COLOR support, as this is the optimal color encoding for DirectX9.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2024-10-07: DirectX9: Changed default texture sampler to Clamp instead of Repeat/Wrap.
|
||||
// 2024-02-12: DirectX9: Using RGBA format when supported by the driver to avoid CPU side conversion. (#6575)
|
||||
// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11.
|
||||
// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX).
|
||||
// 2021-06-25: DirectX9: Explicitly disable texture state stages after >= 1.
|
||||
// 2021-05-19: DirectX9: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
|
||||
// 2021-04-23: DirectX9: Explicitly setting up more graphics states to increase compatibility with unusual non-default states.
|
||||
// 2021-03-18: DirectX9: Calling IDirect3DStateBlock9::Capture() after CreateStateBlock() as a workaround for state restoring issues (see #3857).
|
||||
// 2021-03-03: DirectX9: Added support for IMGUI_USE_BGRA_PACKED_COLOR in user's imconfig file.
|
||||
// 2021-02-18: DirectX9: Change blending equation to preserve alpha in output buffer.
|
||||
// 2019-05-29: DirectX9: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
|
||||
// 2019-04-30: DirectX9: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
|
||||
// 2019-03-29: Misc: Fixed erroneous assert in ImGui_ImplDX9_InvalidateDeviceObjects().
|
||||
// 2019-01-16: Misc: Disabled fog before drawing UI's. Fixes issue #2288.
|
||||
// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
|
||||
// 2018-06-08: Misc: Extracted imgui_impl_dx9.cpp/.h away from the old combined DX9+Win32 example.
|
||||
// 2018-06-08: DirectX9: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle.
|
||||
// 2018-05-07: Render: Saving/restoring Transform because they don't seem to be included in the StateBlock. Setting shading mode to Gouraud.
|
||||
// 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplDX9_RenderDrawData() in the .h file so you can call it yourself.
|
||||
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_impl_dx9.h"
|
||||
|
||||
// DirectX
|
||||
#include <d3d9.h>
|
||||
|
||||
// DirectX data
|
||||
struct ImGui_ImplDX9_Data
|
||||
{
|
||||
LPDIRECT3DDEVICE9 pd3dDevice;
|
||||
LPDIRECT3DVERTEXBUFFER9 pVB;
|
||||
LPDIRECT3DINDEXBUFFER9 pIB;
|
||||
LPDIRECT3DTEXTURE9 FontTexture;
|
||||
int VertexBufferSize;
|
||||
int IndexBufferSize;
|
||||
bool HasRgbaSupport;
|
||||
|
||||
ImGui_ImplDX9_Data() { memset((void*)this, 0, sizeof(*this)); VertexBufferSize = 5000; IndexBufferSize = 10000; }
|
||||
};
|
||||
|
||||
struct CUSTOMVERTEX
|
||||
{
|
||||
float pos[3];
|
||||
D3DCOLOR col;
|
||||
float uv[2];
|
||||
};
|
||||
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE|D3DFVF_TEX1)
|
||||
|
||||
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
|
||||
#define IMGUI_COL_TO_DX9_ARGB(_COL) (_COL)
|
||||
#else
|
||||
#define IMGUI_COL_TO_DX9_ARGB(_COL) (((_COL) & 0xFF00FF00) | (((_COL) & 0xFF0000) >> 16) | (((_COL) & 0xFF) << 16))
|
||||
#endif
|
||||
|
||||
// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts
|
||||
// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
|
||||
static ImGui_ImplDX9_Data* ImGui_ImplDX9_GetBackendData()
|
||||
{
|
||||
return ImGui::GetCurrentContext() ? (ImGui_ImplDX9_Data*)ImGui::GetIO().BackendRendererUserData : nullptr;
|
||||
}
|
||||
|
||||
// Functions
|
||||
static void ImGui_ImplDX9_SetupRenderState(ImDrawData* draw_data)
|
||||
{
|
||||
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
|
||||
|
||||
// Setup viewport
|
||||
D3DVIEWPORT9 vp;
|
||||
vp.X = vp.Y = 0;
|
||||
vp.Width = (DWORD)draw_data->DisplaySize.x;
|
||||
vp.Height = (DWORD)draw_data->DisplaySize.y;
|
||||
vp.MinZ = 0.0f;
|
||||
vp.MaxZ = 1.0f;
|
||||
|
||||
LPDIRECT3DDEVICE9 device = bd->pd3dDevice;
|
||||
device->SetViewport(&vp);
|
||||
|
||||
// Setup render state: fixed-pipeline, alpha-blending, no face culling, no depth testing, shade mode (for gradient), bilinear sampling.
|
||||
device->SetPixelShader(nullptr);
|
||||
device->SetVertexShader(nullptr);
|
||||
device->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
|
||||
device->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_GOURAUD);
|
||||
device->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
|
||||
device->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE);
|
||||
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
||||
device->SetRenderState(D3DRS_ZENABLE, FALSE);
|
||||
device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
|
||||
device->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD);
|
||||
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
||||
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
||||
device->SetRenderState(D3DRS_SEPARATEALPHABLENDENABLE, TRUE);
|
||||
device->SetRenderState(D3DRS_SRCBLENDALPHA, D3DBLEND_ONE);
|
||||
device->SetRenderState(D3DRS_DESTBLENDALPHA, D3DBLEND_INVSRCALPHA);
|
||||
device->SetRenderState(D3DRS_SCISSORTESTENABLE, TRUE);
|
||||
device->SetRenderState(D3DRS_FOGENABLE, FALSE);
|
||||
device->SetRenderState(D3DRS_RANGEFOGENABLE, FALSE);
|
||||
device->SetRenderState(D3DRS_SPECULARENABLE, FALSE);
|
||||
device->SetRenderState(D3DRS_STENCILENABLE, FALSE);
|
||||
device->SetRenderState(D3DRS_CLIPPING, TRUE);
|
||||
device->SetRenderState(D3DRS_LIGHTING, FALSE);
|
||||
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
|
||||
device->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
|
||||
device->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
|
||||
device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
|
||||
device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
|
||||
device->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP);
|
||||
device->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP);
|
||||
|
||||
// Setup orthographic projection matrix
|
||||
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps.
|
||||
// Being agnostic of whether <d3dx9.h> or <DirectXMath.h> can be used, we aren't relying on D3DXMatrixIdentity()/D3DXMatrixOrthoOffCenterLH() or DirectX::XMMatrixIdentity()/DirectX::XMMatrixOrthographicOffCenterLH()
|
||||
{
|
||||
float L = draw_data->DisplayPos.x + 0.5f;
|
||||
float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x + 0.5f;
|
||||
float T = draw_data->DisplayPos.y + 0.5f;
|
||||
float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y + 0.5f;
|
||||
D3DMATRIX mat_identity = { { { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f } } };
|
||||
D3DMATRIX mat_projection =
|
||||
{ { {
|
||||
2.0f/(R-L), 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 2.0f/(T-B), 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 0.5f, 0.0f,
|
||||
(L+R)/(L-R), (T+B)/(B-T), 0.5f, 1.0f
|
||||
} } };
|
||||
device->SetTransform(D3DTS_WORLD, &mat_identity);
|
||||
device->SetTransform(D3DTS_VIEW, &mat_identity);
|
||||
device->SetTransform(D3DTS_PROJECTION, &mat_projection);
|
||||
}
|
||||
}
|
||||
|
||||
// Render function.
|
||||
void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data)
|
||||
{
|
||||
// Avoid rendering when minimized
|
||||
if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f)
|
||||
return;
|
||||
|
||||
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
|
||||
LPDIRECT3DDEVICE9 device = bd->pd3dDevice;
|
||||
|
||||
// Create and grow buffers if needed
|
||||
if (!bd->pVB || bd->VertexBufferSize < draw_data->TotalVtxCount)
|
||||
{
|
||||
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
|
||||
bd->VertexBufferSize = draw_data->TotalVtxCount + 5000;
|
||||
if (device->CreateVertexBuffer(bd->VertexBufferSize * sizeof(CUSTOMVERTEX), D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &bd->pVB, nullptr) < 0)
|
||||
return;
|
||||
}
|
||||
if (!bd->pIB || bd->IndexBufferSize < draw_data->TotalIdxCount)
|
||||
{
|
||||
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
|
||||
bd->IndexBufferSize = draw_data->TotalIdxCount + 10000;
|
||||
if (device->CreateIndexBuffer(bd->IndexBufferSize * sizeof(ImDrawIdx), D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY, sizeof(ImDrawIdx) == 2 ? D3DFMT_INDEX16 : D3DFMT_INDEX32, D3DPOOL_DEFAULT, &bd->pIB, nullptr) < 0)
|
||||
return;
|
||||
}
|
||||
|
||||
// Backup the DX9 state
|
||||
IDirect3DStateBlock9* state_block = nullptr;
|
||||
if (device->CreateStateBlock(D3DSBT_ALL, &state_block) < 0)
|
||||
return;
|
||||
if (state_block->Capture() < 0)
|
||||
{
|
||||
state_block->Release();
|
||||
return;
|
||||
}
|
||||
|
||||
// Backup the DX9 transform (DX9 documentation suggests that it is included in the StateBlock but it doesn't appear to)
|
||||
D3DMATRIX last_world, last_view, last_projection;
|
||||
device->GetTransform(D3DTS_WORLD, &last_world);
|
||||
device->GetTransform(D3DTS_VIEW, &last_view);
|
||||
device->GetTransform(D3DTS_PROJECTION, &last_projection);
|
||||
|
||||
// Allocate buffers
|
||||
CUSTOMVERTEX* vtx_dst;
|
||||
ImDrawIdx* idx_dst;
|
||||
if (bd->pVB->Lock(0, (UINT)(draw_data->TotalVtxCount * sizeof(CUSTOMVERTEX)), (void**)&vtx_dst, D3DLOCK_DISCARD) < 0)
|
||||
{
|
||||
state_block->Release();
|
||||
return;
|
||||
}
|
||||
if (bd->pIB->Lock(0, (UINT)(draw_data->TotalIdxCount * sizeof(ImDrawIdx)), (void**)&idx_dst, D3DLOCK_DISCARD) < 0)
|
||||
{
|
||||
bd->pVB->Unlock();
|
||||
state_block->Release();
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy and convert all vertices into a single contiguous buffer, convert colors to DX9 default format.
|
||||
// FIXME-OPT: This is a minor waste of resource, the ideal is to use imconfig.h and
|
||||
// 1) to avoid repacking colors: #define IMGUI_USE_BGRA_PACKED_COLOR
|
||||
// 2) to avoid repacking vertices: #define IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT struct ImDrawVert { ImVec2 pos; float z; ImU32 col; ImVec2 uv; }
|
||||
for (int n = 0; n < draw_data->CmdListsCount; n++)
|
||||
{
|
||||
const ImDrawList* draw_list = draw_data->CmdLists[n];
|
||||
const ImDrawVert* vtx_src = draw_list->VtxBuffer.Data;
|
||||
for (int i = 0; i < draw_list->VtxBuffer.Size; i++)
|
||||
{
|
||||
vtx_dst->pos[0] = vtx_src->pos.x;
|
||||
vtx_dst->pos[1] = vtx_src->pos.y;
|
||||
vtx_dst->pos[2] = 0.0f;
|
||||
vtx_dst->col = IMGUI_COL_TO_DX9_ARGB(vtx_src->col);
|
||||
vtx_dst->uv[0] = vtx_src->uv.x;
|
||||
vtx_dst->uv[1] = vtx_src->uv.y;
|
||||
vtx_dst++;
|
||||
vtx_src++;
|
||||
}
|
||||
memcpy(idx_dst, draw_list->IdxBuffer.Data, draw_list->IdxBuffer.Size * sizeof(ImDrawIdx));
|
||||
idx_dst += draw_list->IdxBuffer.Size;
|
||||
}
|
||||
bd->pVB->Unlock();
|
||||
bd->pIB->Unlock();
|
||||
device->SetStreamSource(0, bd->pVB, 0, sizeof(CUSTOMVERTEX));
|
||||
device->SetIndices(bd->pIB);
|
||||
device->SetFVF(D3DFVF_CUSTOMVERTEX);
|
||||
|
||||
// Setup desired DX state
|
||||
ImGui_ImplDX9_SetupRenderState(draw_data);
|
||||
|
||||
// Render command lists
|
||||
// (Because we merged all buffers into a single one, we maintain our own offset into them)
|
||||
int global_vtx_offset = 0;
|
||||
int global_idx_offset = 0;
|
||||
ImVec2 clip_off = draw_data->DisplayPos;
|
||||
for (int n = 0; n < draw_data->CmdListsCount; n++)
|
||||
{
|
||||
const ImDrawList* draw_list = draw_data->CmdLists[n];
|
||||
for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++)
|
||||
{
|
||||
const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i];
|
||||
if (pcmd->UserCallback != nullptr)
|
||||
{
|
||||
// User callback, registered via ImDrawList::AddCallback()
|
||||
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.)
|
||||
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState)
|
||||
ImGui_ImplDX9_SetupRenderState(draw_data);
|
||||
else
|
||||
pcmd->UserCallback(draw_list, pcmd);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Project scissor/clipping rectangles into framebuffer space
|
||||
ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y);
|
||||
ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y);
|
||||
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
|
||||
continue;
|
||||
|
||||
// Apply scissor/clipping rectangle
|
||||
const RECT r = { (LONG)clip_min.x, (LONG)clip_min.y, (LONG)clip_max.x, (LONG)clip_max.y };
|
||||
device->SetScissorRect(&r);
|
||||
|
||||
// Bind texture, Draw
|
||||
const LPDIRECT3DTEXTURE9 texture = (LPDIRECT3DTEXTURE9)pcmd->GetTexID();
|
||||
device->SetTexture(0, texture);
|
||||
device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, pcmd->VtxOffset + global_vtx_offset, 0, (UINT)draw_list->VtxBuffer.Size, pcmd->IdxOffset + global_idx_offset, pcmd->ElemCount / 3);
|
||||
}
|
||||
}
|
||||
global_idx_offset += draw_list->IdxBuffer.Size;
|
||||
global_vtx_offset += draw_list->VtxBuffer.Size;
|
||||
}
|
||||
|
||||
// Restore the DX9 transform
|
||||
device->SetTransform(D3DTS_WORLD, &last_world);
|
||||
device->SetTransform(D3DTS_VIEW, &last_view);
|
||||
device->SetTransform(D3DTS_PROJECTION, &last_projection);
|
||||
|
||||
// Restore the DX9 state
|
||||
state_block->Apply();
|
||||
state_block->Release();
|
||||
}
|
||||
|
||||
static bool ImGui_ImplDX9_CheckFormatSupport(LPDIRECT3DDEVICE9 pDevice, D3DFORMAT format)
|
||||
{
|
||||
LPDIRECT3D9 pd3d = nullptr;
|
||||
if (pDevice->GetDirect3D(&pd3d) != D3D_OK)
|
||||
return false;
|
||||
D3DDEVICE_CREATION_PARAMETERS param = {};
|
||||
D3DDISPLAYMODE mode = {};
|
||||
if (pDevice->GetCreationParameters(¶m) != D3D_OK || pDevice->GetDisplayMode(0, &mode) != D3D_OK)
|
||||
{
|
||||
pd3d->Release();
|
||||
return false;
|
||||
}
|
||||
// Font texture should support linear filter, color blend and write to render-target
|
||||
bool support = (pd3d->CheckDeviceFormat(param.AdapterOrdinal, param.DeviceType, mode.Format, D3DUSAGE_DYNAMIC | D3DUSAGE_QUERY_FILTER | D3DUSAGE_QUERY_POSTPIXELSHADER_BLENDING, D3DRTYPE_TEXTURE, format)) == D3D_OK;
|
||||
pd3d->Release();
|
||||
return support;
|
||||
}
|
||||
|
||||
bool ImGui_ImplDX9_Init(IDirect3DDevice9* device)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
IMGUI_CHECKVERSION();
|
||||
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
|
||||
|
||||
// Setup backend capabilities flags
|
||||
ImGui_ImplDX9_Data* bd = IM_NEW(ImGui_ImplDX9_Data)();
|
||||
io.BackendRendererUserData = (void*)bd;
|
||||
io.BackendRendererName = "imgui_impl_dx9";
|
||||
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
|
||||
|
||||
bd->pd3dDevice = device;
|
||||
bd->pd3dDevice->AddRef();
|
||||
bd->HasRgbaSupport = ImGui_ImplDX9_CheckFormatSupport(bd->pd3dDevice, D3DFMT_A8B8G8R8);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplDX9_Shutdown()
|
||||
{
|
||||
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
ImGui_ImplDX9_InvalidateDeviceObjects();
|
||||
if (bd->pd3dDevice) { bd->pd3dDevice->Release(); }
|
||||
io.BackendRendererName = nullptr;
|
||||
io.BackendRendererUserData = nullptr;
|
||||
io.BackendFlags &= ~ImGuiBackendFlags_RendererHasVtxOffset;
|
||||
IM_DELETE(bd);
|
||||
}
|
||||
|
||||
// Convert RGBA32 to BGRA32 (because RGBA32 is not well supported by DX9 devices)
|
||||
static void ImGui_ImplDX9_CopyTextureRegion(bool tex_use_colors, ImU32* src, int src_pitch, ImU32* dst, int dst_pitch, int w, int h)
|
||||
{
|
||||
#ifndef IMGUI_USE_BGRA_PACKED_COLOR
|
||||
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
|
||||
const bool convert_rgba_to_bgra = (!bd->HasRgbaSupport && tex_use_colors);
|
||||
#else
|
||||
const bool convert_rgba_to_bgra = false;
|
||||
IM_UNUSED(tex_use_colors);
|
||||
#endif
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
ImU32* src_p = (ImU32*)((unsigned char*)src + src_pitch * y);
|
||||
ImU32* dst_p = (ImU32*)((unsigned char*)dst + dst_pitch * y);
|
||||
if (convert_rgba_to_bgra)
|
||||
for (int x = w; x > 0; x--, src_p++, dst_p++) // Convert copy
|
||||
*dst_p = IMGUI_COL_TO_DX9_ARGB(*src_p);
|
||||
else
|
||||
memcpy(dst_p, src_p, w * 4); // Raw copy
|
||||
}
|
||||
}
|
||||
|
||||
static bool ImGui_ImplDX9_CreateFontsTexture()
|
||||
{
|
||||
// Build texture atlas
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
|
||||
unsigned char* pixels;
|
||||
int width, height, bytes_per_pixel;
|
||||
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height, &bytes_per_pixel);
|
||||
|
||||
// Upload texture to graphics system
|
||||
bd->FontTexture = nullptr;
|
||||
if (bd->pd3dDevice->CreateTexture(width, height, 1, D3DUSAGE_DYNAMIC, bd->HasRgbaSupport ? D3DFMT_A8B8G8R8 : D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &bd->FontTexture, nullptr) < 0)
|
||||
return false;
|
||||
D3DLOCKED_RECT tex_locked_rect;
|
||||
if (bd->FontTexture->LockRect(0, &tex_locked_rect, nullptr, 0) != D3D_OK)
|
||||
return false;
|
||||
ImGui_ImplDX9_CopyTextureRegion(io.Fonts->TexPixelsUseColors, (ImU32*)pixels, width * bytes_per_pixel, (ImU32*)tex_locked_rect.pBits, (int)tex_locked_rect.Pitch, width, height);
|
||||
bd->FontTexture->UnlockRect(0);
|
||||
|
||||
// Store our identifier
|
||||
io.Fonts->SetTexID((ImTextureID)bd->FontTexture);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImGui_ImplDX9_CreateDeviceObjects()
|
||||
{
|
||||
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
|
||||
if (!bd || !bd->pd3dDevice)
|
||||
return false;
|
||||
if (!ImGui_ImplDX9_CreateFontsTexture())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplDX9_InvalidateDeviceObjects()
|
||||
{
|
||||
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
|
||||
if (!bd || !bd->pd3dDevice)
|
||||
return;
|
||||
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
|
||||
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
|
||||
if (bd->FontTexture) { bd->FontTexture->Release(); bd->FontTexture = nullptr; ImGui::GetIO().Fonts->SetTexID(0); } // We copied bd->pFontTextureView to io.Fonts->TexID so let's clear that as well.
|
||||
}
|
||||
|
||||
void ImGui_ImplDX9_NewFrame()
|
||||
{
|
||||
ImGui_ImplDX9_Data* bd = ImGui_ImplDX9_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplDX9_Init()?");
|
||||
|
||||
if (!bd->FontTexture)
|
||||
ImGui_ImplDX9_CreateDeviceObjects();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
// dear imgui: Renderer Backend for DirectX9
|
||||
// This needs to be used along with a Platform Backend (e.g. Win32)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'LPDIRECT3DTEXTURE9' as ImTextureID. Read the FAQ about ImTextureID!
|
||||
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
|
||||
// [X] Renderer: IMGUI_USE_BGRA_PACKED_COLOR support, as this is the optimal color encoding for DirectX9.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_IMPL_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
struct IDirect3DDevice9;
|
||||
|
||||
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX9_Init(IDirect3DDevice9* device);
|
||||
IMGUI_IMPL_API void ImGui_ImplDX9_Shutdown();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX9_NewFrame();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data);
|
||||
|
||||
// Use if you want to reset your rendering device without losing Dear ImGui state.
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX9_CreateDeviceObjects();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX9_InvalidateDeviceObjects();
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
+940
@@ -0,0 +1,940 @@
|
||||
// dear imgui: Platform Backend for Windows (standard windows API for 32-bits AND 64-bits applications)
|
||||
// This needs to be used along with a Renderer (e.g. DirectX11, OpenGL3, Vulkan..)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Platform: Clipboard support (for Win32 this is actually part of core dear imgui)
|
||||
// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen.
|
||||
// [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy VK_* values are obsolete since 1.87 and not supported since 1.91.5]
|
||||
// [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
|
||||
// [X] Platform: Mouse cursor shape and visibility (ImGuiBackendFlags_HasMouseCursors). Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
// Configuration flags to add in your imconfig file:
|
||||
//#define IMGUI_IMPL_WIN32_DISABLE_GAMEPAD // Disable gamepad support. This was meaningful before <1.81 but we now load XInput dynamically so the option is now less relevant.
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2024-07-08: Inputs: Fixed ImGuiMod_Super being mapped to VK_APPS instead of VK_LWIN||VK_RWIN. (#7768)
|
||||
// 2023-10-05: Inputs: Added support for extra ImGuiKey values: F13 to F24 function keys, app back/forward keys.
|
||||
// 2023-09-25: Inputs: Synthesize key-down event on key-up for VK_SNAPSHOT / ImGuiKey_PrintScreen as Windows doesn't emit it (same behavior as GLFW/SDL).
|
||||
// 2023-09-07: Inputs: Added support for keyboard codepage conversion for when application is compiled in MBCS mode and using a non-Unicode window.
|
||||
// 2023-04-19: Added ImGui_ImplWin32_InitForOpenGL() to facilitate combining raw Win32/Winapi with OpenGL. (#3218)
|
||||
// 2023-04-04: Inputs: Added support for io.AddMouseSourceEvent() to discriminate ImGuiMouseSource_Mouse/ImGuiMouseSource_TouchScreen/ImGuiMouseSource_Pen. (#2702)
|
||||
// 2023-02-15: Inputs: Use WM_NCMOUSEMOVE / WM_NCMOUSELEAVE to track mouse position over non-client area (e.g. OS decorations) when app is not focused. (#6045, #6162)
|
||||
// 2023-02-02: Inputs: Flipping WM_MOUSEHWHEEL (horizontal mouse-wheel) value to match other backends and offer consistent horizontal scrolling direction. (#4019, #6096, #1463)
|
||||
// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11.
|
||||
// 2022-09-28: Inputs: Convert WM_CHAR values with MultiByteToWideChar() when window class was registered as MBCS (not Unicode).
|
||||
// 2022-09-26: Inputs: Renamed ImGuiKey_ModXXX introduced in 1.87 to ImGuiMod_XXX (old names still supported).
|
||||
// 2022-01-26: Inputs: replaced short-lived io.AddKeyModsEvent() (added two weeks ago) with io.AddKeyEvent() using ImGuiKey_ModXXX flags. Sorry for the confusion.
|
||||
// 2021-01-20: Inputs: calling new io.AddKeyAnalogEvent() for gamepad support, instead of writing directly to io.NavInputs[].
|
||||
// 2022-01-17: Inputs: calling new io.AddMousePosEvent(), io.AddMouseButtonEvent(), io.AddMouseWheelEvent() API (1.87+).
|
||||
// 2022-01-17: Inputs: always update key mods next and before a key event (not in NewFrame) to fix input queue with very low framerates.
|
||||
// 2022-01-12: Inputs: Update mouse inputs using WM_MOUSEMOVE/WM_MOUSELEAVE + fallback to provide it when focused but not hovered/captured. More standard and will allow us to pass it to future input queue API.
|
||||
// 2022-01-12: Inputs: Maintain our own copy of MouseButtonsDown mask instead of using ImGui::IsAnyMouseDown() which will be obsoleted.
|
||||
// 2022-01-10: Inputs: calling new io.AddKeyEvent(), io.AddKeyModsEvent() + io.SetKeyEventNativeData() API (1.87+). Support for full ImGuiKey range.
|
||||
// 2021-12-16: Inputs: Fill VK_LCONTROL/VK_RCONTROL/VK_LSHIFT/VK_RSHIFT/VK_LMENU/VK_RMENU for completeness.
|
||||
// 2021-08-17: Calling io.AddFocusEvent() on WM_SETFOCUS/WM_KILLFOCUS messages.
|
||||
// 2021-08-02: Inputs: Fixed keyboard modifiers being reported when host window doesn't have focus.
|
||||
// 2021-07-29: Inputs: MousePos is correctly reported when the host platform window is hovered but not focused (using TrackMouseEvent() to receive WM_MOUSELEAVE events).
|
||||
// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX).
|
||||
// 2021-06-08: Fixed ImGui_ImplWin32_EnableDpiAwareness() and ImGui_ImplWin32_GetDpiScaleForMonitor() to handle Windows 8.1/10 features without a manifest (per-monitor DPI, and properly calls SetProcessDpiAwareness() on 8.1).
|
||||
// 2021-03-23: Inputs: Clearing keyboard down array when losing focus (WM_KILLFOCUS).
|
||||
// 2021-02-18: Added ImGui_ImplWin32_EnableAlphaCompositing(). Non Visual Studio users will need to link with dwmapi.lib (MinGW/gcc: use -ldwmapi).
|
||||
// 2021-02-17: Fixed ImGui_ImplWin32_EnableDpiAwareness() attempting to get SetProcessDpiAwareness from shcore.dll on Windows 8 whereas it is only supported on Windows 8.1.
|
||||
// 2021-01-25: Inputs: Dynamically loading XInput DLL.
|
||||
// 2020-12-04: Misc: Fixed setting of io.DisplaySize to invalid/uninitialized data when after hwnd has been closed.
|
||||
// 2020-03-03: Inputs: Calling AddInputCharacterUTF16() to support surrogate pairs leading to codepoint >= 0x10000 (for more complete CJK inputs)
|
||||
// 2020-02-17: Added ImGui_ImplWin32_EnableDpiAwareness(), ImGui_ImplWin32_GetDpiScaleForHwnd(), ImGui_ImplWin32_GetDpiScaleForMonitor() helper functions.
|
||||
// 2020-01-14: Inputs: Added support for #define IMGUI_IMPL_WIN32_DISABLE_GAMEPAD/IMGUI_IMPL_WIN32_DISABLE_LINKING_XINPUT.
|
||||
// 2019-12-05: Inputs: Added support for ImGuiMouseCursor_NotAllowed mouse cursor.
|
||||
// 2019-05-11: Inputs: Don't filter value from WM_CHAR before calling AddInputCharacter().
|
||||
// 2019-01-17: Misc: Using GetForegroundWindow()+IsChild() instead of GetActiveWindow() to be compatible with windows created in a different thread or parent.
|
||||
// 2019-01-17: Inputs: Added support for mouse buttons 4 and 5 via WM_XBUTTON* messages.
|
||||
// 2019-01-15: Inputs: Added support for XInput gamepads (if ImGuiConfigFlags_NavEnableGamepad is set by user application).
|
||||
// 2018-11-30: Misc: Setting up io.BackendPlatformName so it can be displayed in the About Window.
|
||||
// 2018-06-29: Inputs: Added support for the ImGuiMouseCursor_Hand cursor.
|
||||
// 2018-06-10: Inputs: Fixed handling of mouse wheel messages to support fine position messages (typically sent by track-pads).
|
||||
// 2018-06-08: Misc: Extracted imgui_impl_win32.cpp/.h away from the old combined DX9/DX10/DX11/DX12 examples.
|
||||
// 2018-03-20: Misc: Setup io.BackendFlags ImGuiBackendFlags_HasMouseCursors and ImGuiBackendFlags_HasSetMousePos flags + honor ImGuiConfigFlags_NoMouseCursorChange flag.
|
||||
// 2018-02-20: Inputs: Added support for mouse cursors (ImGui::GetMouseCursor() value and WM_SETCURSOR message handling).
|
||||
// 2018-02-06: Inputs: Added mapping for ImGuiKey_Space.
|
||||
// 2018-02-06: Inputs: Honoring the io.WantSetMousePos by repositioning the mouse (when using navigation and ImGuiConfigFlags_NavMoveMouse is set).
|
||||
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
|
||||
// 2018-01-20: Inputs: Added Horizontal Mouse Wheel support.
|
||||
// 2018-01-08: Inputs: Added mapping for ImGuiKey_Insert.
|
||||
// 2018-01-05: Inputs: Added WM_LBUTTONDBLCLK double-click handlers for window classes with the CS_DBLCLKS flag.
|
||||
// 2017-10-23: Inputs: Added WM_SYSKEYDOWN / WM_SYSKEYUP handlers so e.g. the VK_MENU key can be read.
|
||||
// 2017-10-23: Inputs: Using Win32 ::SetCapture/::GetCapture() to retrieve mouse positions outside the client area when dragging.
|
||||
// 2016-11-12: Inputs: Only call Win32 ::SetCursor(nullptr) when io.MouseDrawCursor is set.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_impl_win32.h"
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <windowsx.h> // GET_X_LPARAM(), GET_Y_LPARAM()
|
||||
#include <tchar.h>
|
||||
#include <dwmapi.h>
|
||||
|
||||
// Using XInput for gamepad (will load DLL dynamically)
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
#include <xinput.h>
|
||||
typedef DWORD(WINAPI* PFN_XInputGetCapabilities)(DWORD, DWORD, XINPUT_CAPABILITIES*);
|
||||
typedef DWORD(WINAPI* PFN_XInputGetState)(DWORD, XINPUT_STATE*);
|
||||
#endif
|
||||
|
||||
// Clang/GCC warnings with -Weverything
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader)
|
||||
#endif
|
||||
#if defined(__GNUC__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
|
||||
#pragma GCC diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader)
|
||||
#endif
|
||||
|
||||
struct ImGui_ImplWin32_Data
|
||||
{
|
||||
HWND hWnd;
|
||||
HWND MouseHwnd;
|
||||
int MouseTrackedArea; // 0: not tracked, 1: client area, 2: non-client area
|
||||
int MouseButtonsDown;
|
||||
INT64 Time;
|
||||
INT64 TicksPerSecond;
|
||||
ImGuiMouseCursor LastMouseCursor;
|
||||
UINT32 KeyboardCodePage;
|
||||
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
bool HasGamepad;
|
||||
bool WantUpdateHasGamepad;
|
||||
HMODULE XInputDLL;
|
||||
PFN_XInputGetCapabilities XInputGetCapabilities;
|
||||
PFN_XInputGetState XInputGetState;
|
||||
#endif
|
||||
|
||||
ImGui_ImplWin32_Data() { memset((void*)this, 0, sizeof(*this)); }
|
||||
};
|
||||
|
||||
// Backend data stored in io.BackendPlatformUserData to allow support for multiple Dear ImGui contexts
|
||||
// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
|
||||
// FIXME: multi-context support is not well tested and probably dysfunctional in this backend.
|
||||
// FIXME: some shared resources (mouse cursor shape, gamepad) are mishandled when using multi-context.
|
||||
static ImGui_ImplWin32_Data* ImGui_ImplWin32_GetBackendData()
|
||||
{
|
||||
return ImGui::GetCurrentContext() ? (ImGui_ImplWin32_Data*)ImGui::GetIO().BackendPlatformUserData : nullptr;
|
||||
}
|
||||
static ImGui_ImplWin32_Data* ImGui_ImplWin32_GetBackendData(ImGuiIO& io)
|
||||
{
|
||||
return (ImGui_ImplWin32_Data*)io.BackendPlatformUserData;
|
||||
}
|
||||
|
||||
// Functions
|
||||
static void ImGui_ImplWin32_UpdateKeyboardCodePage(ImGuiIO& io)
|
||||
{
|
||||
// Retrieve keyboard code page, required for handling of non-Unicode Windows.
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
HKL keyboard_layout = ::GetKeyboardLayout(0);
|
||||
LCID keyboard_lcid = MAKELCID(HIWORD(keyboard_layout), SORT_DEFAULT);
|
||||
if (::GetLocaleInfoA(keyboard_lcid, (LOCALE_RETURN_NUMBER | LOCALE_IDEFAULTANSICODEPAGE), (LPSTR)&bd->KeyboardCodePage, sizeof(bd->KeyboardCodePage)) == 0)
|
||||
bd->KeyboardCodePage = CP_ACP; // Fallback to default ANSI code page when fails.
|
||||
}
|
||||
|
||||
static bool ImGui_ImplWin32_InitEx(void* hwnd, bool platform_has_own_dc)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
IMGUI_CHECKVERSION();
|
||||
IM_ASSERT(io.BackendPlatformUserData == nullptr && "Already initialized a platform backend!");
|
||||
|
||||
INT64 perf_frequency, perf_counter;
|
||||
if (!::QueryPerformanceFrequency((LARGE_INTEGER*)&perf_frequency))
|
||||
return false;
|
||||
if (!::QueryPerformanceCounter((LARGE_INTEGER*)&perf_counter))
|
||||
return false;
|
||||
|
||||
// Setup backend capabilities flags
|
||||
ImGui_ImplWin32_Data* bd = IM_NEW(ImGui_ImplWin32_Data)();
|
||||
io.BackendPlatformUserData = (void*)bd;
|
||||
io.BackendPlatformName = "imgui_impl_win32";
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional)
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used)
|
||||
|
||||
bd->hWnd = (HWND)hwnd;
|
||||
bd->TicksPerSecond = perf_frequency;
|
||||
bd->Time = perf_counter;
|
||||
bd->LastMouseCursor = ImGuiMouseCursor_COUNT;
|
||||
ImGui_ImplWin32_UpdateKeyboardCodePage(io);
|
||||
|
||||
// Set platform dependent data in viewport
|
||||
ImGuiViewport* main_viewport = ImGui::GetMainViewport();
|
||||
main_viewport->PlatformHandle = main_viewport->PlatformHandleRaw = (void*)bd->hWnd;
|
||||
IM_UNUSED(platform_has_own_dc); // Used in 'docking' branch
|
||||
|
||||
// Dynamically load XInput library
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
bd->WantUpdateHasGamepad = true;
|
||||
const char* xinput_dll_names[] =
|
||||
{
|
||||
"xinput1_4.dll", // Windows 8+
|
||||
"xinput1_3.dll", // DirectX SDK
|
||||
"xinput9_1_0.dll", // Windows Vista, Windows 7
|
||||
"xinput1_2.dll", // DirectX SDK
|
||||
"xinput1_1.dll" // DirectX SDK
|
||||
};
|
||||
for (int n = 0; n < IM_ARRAYSIZE(xinput_dll_names); n++)
|
||||
if (HMODULE dll = ::LoadLibraryA(xinput_dll_names[n]))
|
||||
{
|
||||
bd->XInputDLL = dll;
|
||||
bd->XInputGetCapabilities = (PFN_XInputGetCapabilities)::GetProcAddress(dll, "XInputGetCapabilities");
|
||||
bd->XInputGetState = (PFN_XInputGetState)::GetProcAddress(dll, "XInputGetState");
|
||||
break;
|
||||
}
|
||||
#endif // IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_Init(void* hwnd)
|
||||
{
|
||||
return ImGui_ImplWin32_InitEx(hwnd, false);
|
||||
}
|
||||
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_InitForOpenGL(void* hwnd)
|
||||
{
|
||||
// OpenGL needs CS_OWNDC
|
||||
return ImGui_ImplWin32_InitEx(hwnd, true);
|
||||
}
|
||||
|
||||
void ImGui_ImplWin32_Shutdown()
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "No platform backend to shutdown, or already shutdown?");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Unload XInput library
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
if (bd->XInputDLL)
|
||||
::FreeLibrary(bd->XInputDLL);
|
||||
#endif // IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
|
||||
io.BackendPlatformName = nullptr;
|
||||
io.BackendPlatformUserData = nullptr;
|
||||
io.BackendFlags &= ~(ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_HasSetMousePos | ImGuiBackendFlags_HasGamepad);
|
||||
IM_DELETE(bd);
|
||||
}
|
||||
|
||||
static bool ImGui_ImplWin32_UpdateMouseCursor(ImGuiIO& io, ImGuiMouseCursor imgui_cursor)
|
||||
{
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange)
|
||||
return false;
|
||||
|
||||
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor)
|
||||
{
|
||||
// Hide OS mouse cursor if imgui is drawing it or if it wants no cursor
|
||||
::SetCursor(nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show OS mouse cursor
|
||||
LPTSTR win32_cursor = IDC_ARROW;
|
||||
switch (imgui_cursor)
|
||||
{
|
||||
case ImGuiMouseCursor_Arrow: win32_cursor = IDC_ARROW; break;
|
||||
case ImGuiMouseCursor_TextInput: win32_cursor = IDC_IBEAM; break;
|
||||
case ImGuiMouseCursor_ResizeAll: win32_cursor = IDC_SIZEALL; break;
|
||||
case ImGuiMouseCursor_ResizeEW: win32_cursor = IDC_SIZEWE; break;
|
||||
case ImGuiMouseCursor_ResizeNS: win32_cursor = IDC_SIZENS; break;
|
||||
case ImGuiMouseCursor_ResizeNESW: win32_cursor = IDC_SIZENESW; break;
|
||||
case ImGuiMouseCursor_ResizeNWSE: win32_cursor = IDC_SIZENWSE; break;
|
||||
case ImGuiMouseCursor_Hand: win32_cursor = IDC_HAND; break;
|
||||
case ImGuiMouseCursor_NotAllowed: win32_cursor = IDC_NO; break;
|
||||
}
|
||||
::SetCursor(::LoadCursor(nullptr, win32_cursor));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool IsVkDown(int vk)
|
||||
{
|
||||
return (::GetKeyState(vk) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_AddKeyEvent(ImGuiIO& io, ImGuiKey key, bool down, int native_keycode, int native_scancode = -1)
|
||||
{
|
||||
io.AddKeyEvent(key, down);
|
||||
io.SetKeyEventNativeData(key, native_keycode, native_scancode); // To support legacy indexing (<1.87 user code)
|
||||
IM_UNUSED(native_scancode);
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_ProcessKeyEventsWorkarounds(ImGuiIO& io)
|
||||
{
|
||||
// Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one.
|
||||
if (ImGui::IsKeyDown(ImGuiKey_LeftShift) && !IsVkDown(VK_LSHIFT))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftShift, false, VK_LSHIFT);
|
||||
if (ImGui::IsKeyDown(ImGuiKey_RightShift) && !IsVkDown(VK_RSHIFT))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightShift, false, VK_RSHIFT);
|
||||
|
||||
// Sometimes WM_KEYUP for Win key is not passed down to the app (e.g. for Win+V on some setups, according to GLFW).
|
||||
if (ImGui::IsKeyDown(ImGuiKey_LeftSuper) && !IsVkDown(VK_LWIN))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftSuper, false, VK_LWIN);
|
||||
if (ImGui::IsKeyDown(ImGuiKey_RightSuper) && !IsVkDown(VK_RWIN))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightSuper, false, VK_RWIN);
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_UpdateKeyModifiers(ImGuiIO& io)
|
||||
{
|
||||
io.AddKeyEvent(ImGuiMod_Ctrl, IsVkDown(VK_CONTROL));
|
||||
io.AddKeyEvent(ImGuiMod_Shift, IsVkDown(VK_SHIFT));
|
||||
io.AddKeyEvent(ImGuiMod_Alt, IsVkDown(VK_MENU));
|
||||
io.AddKeyEvent(ImGuiMod_Super, IsVkDown(VK_LWIN) || IsVkDown(VK_RWIN));
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_UpdateMouseData(ImGuiIO& io)
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
IM_ASSERT(bd->hWnd != 0);
|
||||
|
||||
HWND focused_window = ::GetForegroundWindow();
|
||||
const bool is_app_focused = (focused_window == bd->hWnd);
|
||||
if (is_app_focused)
|
||||
{
|
||||
// (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when io.ConfigNavMoveSetMousePos is enabled by user)
|
||||
if (io.WantSetMousePos)
|
||||
{
|
||||
POINT pos = { (int)io.MousePos.x, (int)io.MousePos.y };
|
||||
if (::ClientToScreen(bd->hWnd, &pos))
|
||||
::SetCursorPos(pos.x, pos.y);
|
||||
}
|
||||
|
||||
// (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
|
||||
// This also fills a short gap when clicking non-client area: WM_NCMOUSELEAVE -> modal OS move -> gap -> WM_NCMOUSEMOVE
|
||||
if (!io.WantSetMousePos && bd->MouseTrackedArea == 0)
|
||||
{
|
||||
POINT pos;
|
||||
if (::GetCursorPos(&pos) && ::ScreenToClient(bd->hWnd, &pos))
|
||||
io.AddMousePosEvent((float)pos.x, (float)pos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gamepad navigation mapping
|
||||
static void ImGui_ImplWin32_UpdateGamepads(ImGuiIO& io)
|
||||
{
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
//if ((io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) == 0) // FIXME: Technically feeding gamepad shouldn't depend on this now that they are regular inputs.
|
||||
// return;
|
||||
|
||||
// Calling XInputGetState() every frame on disconnected gamepads is unfortunately too slow.
|
||||
// Instead we refresh gamepad availability by calling XInputGetCapabilities() _only_ after receiving WM_DEVICECHANGE.
|
||||
if (bd->WantUpdateHasGamepad)
|
||||
{
|
||||
XINPUT_CAPABILITIES caps = {};
|
||||
bd->HasGamepad = bd->XInputGetCapabilities ? (bd->XInputGetCapabilities(0, XINPUT_FLAG_GAMEPAD, &caps) == ERROR_SUCCESS) : false;
|
||||
bd->WantUpdateHasGamepad = false;
|
||||
}
|
||||
|
||||
io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad;
|
||||
XINPUT_STATE xinput_state;
|
||||
XINPUT_GAMEPAD& gamepad = xinput_state.Gamepad;
|
||||
if (!bd->HasGamepad || bd->XInputGetState == nullptr || bd->XInputGetState(0, &xinput_state) != ERROR_SUCCESS)
|
||||
return;
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
|
||||
|
||||
#define IM_SATURATE(V) (V < 0.0f ? 0.0f : V > 1.0f ? 1.0f : V)
|
||||
#define MAP_BUTTON(KEY_NO, BUTTON_ENUM) { io.AddKeyEvent(KEY_NO, (gamepad.wButtons & BUTTON_ENUM) != 0); }
|
||||
#define MAP_ANALOG(KEY_NO, VALUE, V0, V1) { float vn = (float)(VALUE - V0) / (float)(V1 - V0); io.AddKeyAnalogEvent(KEY_NO, vn > 0.10f, IM_SATURATE(vn)); }
|
||||
MAP_BUTTON(ImGuiKey_GamepadStart, XINPUT_GAMEPAD_START);
|
||||
MAP_BUTTON(ImGuiKey_GamepadBack, XINPUT_GAMEPAD_BACK);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceLeft, XINPUT_GAMEPAD_X);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceRight, XINPUT_GAMEPAD_B);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceUp, XINPUT_GAMEPAD_Y);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceDown, XINPUT_GAMEPAD_A);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadLeft, XINPUT_GAMEPAD_DPAD_LEFT);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadRight, XINPUT_GAMEPAD_DPAD_RIGHT);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadUp, XINPUT_GAMEPAD_DPAD_UP);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadDown, XINPUT_GAMEPAD_DPAD_DOWN);
|
||||
MAP_BUTTON(ImGuiKey_GamepadL1, XINPUT_GAMEPAD_LEFT_SHOULDER);
|
||||
MAP_BUTTON(ImGuiKey_GamepadR1, XINPUT_GAMEPAD_RIGHT_SHOULDER);
|
||||
MAP_ANALOG(ImGuiKey_GamepadL2, gamepad.bLeftTrigger, XINPUT_GAMEPAD_TRIGGER_THRESHOLD, 255);
|
||||
MAP_ANALOG(ImGuiKey_GamepadR2, gamepad.bRightTrigger, XINPUT_GAMEPAD_TRIGGER_THRESHOLD, 255);
|
||||
MAP_BUTTON(ImGuiKey_GamepadL3, XINPUT_GAMEPAD_LEFT_THUMB);
|
||||
MAP_BUTTON(ImGuiKey_GamepadR3, XINPUT_GAMEPAD_RIGHT_THUMB);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickLeft, gamepad.sThumbLX, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickRight, gamepad.sThumbLX, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickUp, gamepad.sThumbLY, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickDown, gamepad.sThumbLY, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickLeft, gamepad.sThumbRX, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickRight, gamepad.sThumbRX, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickUp, gamepad.sThumbRY, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickDown, gamepad.sThumbRY, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
#undef MAP_BUTTON
|
||||
#undef MAP_ANALOG
|
||||
#else // #ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
IM_UNUSED(io);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImGui_ImplWin32_NewFrame()
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "Context or backend not initialized? Did you call ImGui_ImplWin32_Init()?");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Setup display size (every frame to accommodate for window resizing)
|
||||
RECT rect = { 0, 0, 0, 0 };
|
||||
::GetClientRect(bd->hWnd, &rect);
|
||||
io.DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top));
|
||||
|
||||
// Setup time step
|
||||
INT64 current_time = 0;
|
||||
::QueryPerformanceCounter((LARGE_INTEGER*)¤t_time);
|
||||
io.DeltaTime = (float)(current_time - bd->Time) / bd->TicksPerSecond;
|
||||
bd->Time = current_time;
|
||||
|
||||
// Update OS mouse position
|
||||
ImGui_ImplWin32_UpdateMouseData(io);
|
||||
|
||||
// Process workarounds for known Windows key handling issues
|
||||
ImGui_ImplWin32_ProcessKeyEventsWorkarounds(io);
|
||||
|
||||
// Update OS mouse cursor with the cursor requested by imgui
|
||||
ImGuiMouseCursor mouse_cursor = io.MouseDrawCursor ? ImGuiMouseCursor_None : ImGui::GetMouseCursor();
|
||||
if (bd->LastMouseCursor != mouse_cursor)
|
||||
{
|
||||
bd->LastMouseCursor = mouse_cursor;
|
||||
ImGui_ImplWin32_UpdateMouseCursor(io, mouse_cursor);
|
||||
}
|
||||
|
||||
// Update game controllers (if enabled and available)
|
||||
ImGui_ImplWin32_UpdateGamepads(io);
|
||||
}
|
||||
|
||||
// Map VK_xxx to ImGuiKey_xxx.
|
||||
// Not static to allow third-party code to use that if they want to (but undocumented)
|
||||
ImGuiKey ImGui_ImplWin32_KeyEventToImGuiKey(WPARAM wParam, LPARAM lParam);
|
||||
ImGuiKey ImGui_ImplWin32_KeyEventToImGuiKey(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// There is no distinct VK_xxx for keypad enter, instead it is VK_RETURN + KF_EXTENDED.
|
||||
if ((wParam == VK_RETURN) && (HIWORD(lParam) & KF_EXTENDED))
|
||||
return ImGuiKey_KeypadEnter;
|
||||
|
||||
switch (wParam)
|
||||
{
|
||||
case VK_TAB: return ImGuiKey_Tab;
|
||||
case VK_LEFT: return ImGuiKey_LeftArrow;
|
||||
case VK_RIGHT: return ImGuiKey_RightArrow;
|
||||
case VK_UP: return ImGuiKey_UpArrow;
|
||||
case VK_DOWN: return ImGuiKey_DownArrow;
|
||||
case VK_PRIOR: return ImGuiKey_PageUp;
|
||||
case VK_NEXT: return ImGuiKey_PageDown;
|
||||
case VK_HOME: return ImGuiKey_Home;
|
||||
case VK_END: return ImGuiKey_End;
|
||||
case VK_INSERT: return ImGuiKey_Insert;
|
||||
case VK_DELETE: return ImGuiKey_Delete;
|
||||
case VK_BACK: return ImGuiKey_Backspace;
|
||||
case VK_SPACE: return ImGuiKey_Space;
|
||||
case VK_RETURN: return ImGuiKey_Enter;
|
||||
case VK_ESCAPE: return ImGuiKey_Escape;
|
||||
case VK_OEM_7: return ImGuiKey_Apostrophe;
|
||||
case VK_OEM_COMMA: return ImGuiKey_Comma;
|
||||
case VK_OEM_MINUS: return ImGuiKey_Minus;
|
||||
case VK_OEM_PERIOD: return ImGuiKey_Period;
|
||||
case VK_OEM_2: return ImGuiKey_Slash;
|
||||
case VK_OEM_1: return ImGuiKey_Semicolon;
|
||||
case VK_OEM_PLUS: return ImGuiKey_Equal;
|
||||
case VK_OEM_4: return ImGuiKey_LeftBracket;
|
||||
case VK_OEM_5: return ImGuiKey_Backslash;
|
||||
case VK_OEM_6: return ImGuiKey_RightBracket;
|
||||
case VK_OEM_3: return ImGuiKey_GraveAccent;
|
||||
case VK_CAPITAL: return ImGuiKey_CapsLock;
|
||||
case VK_SCROLL: return ImGuiKey_ScrollLock;
|
||||
case VK_NUMLOCK: return ImGuiKey_NumLock;
|
||||
case VK_SNAPSHOT: return ImGuiKey_PrintScreen;
|
||||
case VK_PAUSE: return ImGuiKey_Pause;
|
||||
case VK_NUMPAD0: return ImGuiKey_Keypad0;
|
||||
case VK_NUMPAD1: return ImGuiKey_Keypad1;
|
||||
case VK_NUMPAD2: return ImGuiKey_Keypad2;
|
||||
case VK_NUMPAD3: return ImGuiKey_Keypad3;
|
||||
case VK_NUMPAD4: return ImGuiKey_Keypad4;
|
||||
case VK_NUMPAD5: return ImGuiKey_Keypad5;
|
||||
case VK_NUMPAD6: return ImGuiKey_Keypad6;
|
||||
case VK_NUMPAD7: return ImGuiKey_Keypad7;
|
||||
case VK_NUMPAD8: return ImGuiKey_Keypad8;
|
||||
case VK_NUMPAD9: return ImGuiKey_Keypad9;
|
||||
case VK_DECIMAL: return ImGuiKey_KeypadDecimal;
|
||||
case VK_DIVIDE: return ImGuiKey_KeypadDivide;
|
||||
case VK_MULTIPLY: return ImGuiKey_KeypadMultiply;
|
||||
case VK_SUBTRACT: return ImGuiKey_KeypadSubtract;
|
||||
case VK_ADD: return ImGuiKey_KeypadAdd;
|
||||
case VK_LSHIFT: return ImGuiKey_LeftShift;
|
||||
case VK_LCONTROL: return ImGuiKey_LeftCtrl;
|
||||
case VK_LMENU: return ImGuiKey_LeftAlt;
|
||||
case VK_LWIN: return ImGuiKey_LeftSuper;
|
||||
case VK_RSHIFT: return ImGuiKey_RightShift;
|
||||
case VK_RCONTROL: return ImGuiKey_RightCtrl;
|
||||
case VK_RMENU: return ImGuiKey_RightAlt;
|
||||
case VK_RWIN: return ImGuiKey_RightSuper;
|
||||
case VK_APPS: return ImGuiKey_Menu;
|
||||
case '0': return ImGuiKey_0;
|
||||
case '1': return ImGuiKey_1;
|
||||
case '2': return ImGuiKey_2;
|
||||
case '3': return ImGuiKey_3;
|
||||
case '4': return ImGuiKey_4;
|
||||
case '5': return ImGuiKey_5;
|
||||
case '6': return ImGuiKey_6;
|
||||
case '7': return ImGuiKey_7;
|
||||
case '8': return ImGuiKey_8;
|
||||
case '9': return ImGuiKey_9;
|
||||
case 'A': return ImGuiKey_A;
|
||||
case 'B': return ImGuiKey_B;
|
||||
case 'C': return ImGuiKey_C;
|
||||
case 'D': return ImGuiKey_D;
|
||||
case 'E': return ImGuiKey_E;
|
||||
case 'F': return ImGuiKey_F;
|
||||
case 'G': return ImGuiKey_G;
|
||||
case 'H': return ImGuiKey_H;
|
||||
case 'I': return ImGuiKey_I;
|
||||
case 'J': return ImGuiKey_J;
|
||||
case 'K': return ImGuiKey_K;
|
||||
case 'L': return ImGuiKey_L;
|
||||
case 'M': return ImGuiKey_M;
|
||||
case 'N': return ImGuiKey_N;
|
||||
case 'O': return ImGuiKey_O;
|
||||
case 'P': return ImGuiKey_P;
|
||||
case 'Q': return ImGuiKey_Q;
|
||||
case 'R': return ImGuiKey_R;
|
||||
case 'S': return ImGuiKey_S;
|
||||
case 'T': return ImGuiKey_T;
|
||||
case 'U': return ImGuiKey_U;
|
||||
case 'V': return ImGuiKey_V;
|
||||
case 'W': return ImGuiKey_W;
|
||||
case 'X': return ImGuiKey_X;
|
||||
case 'Y': return ImGuiKey_Y;
|
||||
case 'Z': return ImGuiKey_Z;
|
||||
case VK_F1: return ImGuiKey_F1;
|
||||
case VK_F2: return ImGuiKey_F2;
|
||||
case VK_F3: return ImGuiKey_F3;
|
||||
case VK_F4: return ImGuiKey_F4;
|
||||
case VK_F5: return ImGuiKey_F5;
|
||||
case VK_F6: return ImGuiKey_F6;
|
||||
case VK_F7: return ImGuiKey_F7;
|
||||
case VK_F8: return ImGuiKey_F8;
|
||||
case VK_F9: return ImGuiKey_F9;
|
||||
case VK_F10: return ImGuiKey_F10;
|
||||
case VK_F11: return ImGuiKey_F11;
|
||||
case VK_F12: return ImGuiKey_F12;
|
||||
case VK_F13: return ImGuiKey_F13;
|
||||
case VK_F14: return ImGuiKey_F14;
|
||||
case VK_F15: return ImGuiKey_F15;
|
||||
case VK_F16: return ImGuiKey_F16;
|
||||
case VK_F17: return ImGuiKey_F17;
|
||||
case VK_F18: return ImGuiKey_F18;
|
||||
case VK_F19: return ImGuiKey_F19;
|
||||
case VK_F20: return ImGuiKey_F20;
|
||||
case VK_F21: return ImGuiKey_F21;
|
||||
case VK_F22: return ImGuiKey_F22;
|
||||
case VK_F23: return ImGuiKey_F23;
|
||||
case VK_F24: return ImGuiKey_F24;
|
||||
case VK_BROWSER_BACK: return ImGuiKey_AppBack;
|
||||
case VK_BROWSER_FORWARD: return ImGuiKey_AppForward;
|
||||
default: return ImGuiKey_None;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow compilation with old Windows SDK. MinGW doesn't have default _WIN32_WINNT/WINVER versions.
|
||||
#ifndef WM_MOUSEHWHEEL
|
||||
#define WM_MOUSEHWHEEL 0x020E
|
||||
#endif
|
||||
#ifndef DBT_DEVNODES_CHANGED
|
||||
#define DBT_DEVNODES_CHANGED 0x0007
|
||||
#endif
|
||||
|
||||
// Helper to obtain the source of mouse messages.
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
|
||||
// Prefer to call this at the top of the message handler to avoid the possibility of other Win32 calls interfering with this.
|
||||
static ImGuiMouseSource ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo()
|
||||
{
|
||||
LPARAM extra_info = ::GetMessageExtraInfo();
|
||||
if ((extra_info & 0xFFFFFF80) == 0xFF515700)
|
||||
return ImGuiMouseSource_Pen;
|
||||
if ((extra_info & 0xFFFFFF80) == 0xFF515780)
|
||||
return ImGuiMouseSource_TouchScreen;
|
||||
return ImGuiMouseSource_Mouse;
|
||||
}
|
||||
|
||||
// Win32 message handler (process Win32 mouse/keyboard inputs, etc.)
|
||||
// Call from your application's message handler. Keep calling your message handler unless this function returns TRUE.
|
||||
// When implementing your own backend, you can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if Dear ImGui wants to use your inputs.
|
||||
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
|
||||
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
|
||||
// Generally you may always pass all inputs to Dear ImGui, and hide them from your application based on those two flags.
|
||||
// PS: We treat DBLCLK messages as regular mouse down messages, so this code will work on windows classes that have the CS_DBLCLKS flag set. Our own example app code doesn't set this flag.
|
||||
|
||||
// Copy either line into your .cpp file to forward declare the function:
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Use ImGui::GetCurrentContext()
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandlerEx(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ImGuiIO& io); // Doesn't use ImGui::GetCurrentContext()
|
||||
|
||||
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// Most backends don't have silent checks like this one, but we need it because WndProc are called early in CreateWindow().
|
||||
// We silently allow both context or just only backend data to be nullptr.
|
||||
if (ImGui::GetCurrentContext() == nullptr)
|
||||
return 0;
|
||||
return ImGui_ImplWin32_WndProcHandlerEx(hwnd, msg, wParam, lParam, ImGui::GetIO());
|
||||
}
|
||||
|
||||
// This version is in theory thread-safe in the sense that no path should access ImGui::GetCurrentContext().
|
||||
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandlerEx(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, ImGuiIO& io)
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
if (bd == nullptr)
|
||||
return 0;
|
||||
switch (msg)
|
||||
{
|
||||
case WM_MOUSEMOVE:
|
||||
case WM_NCMOUSEMOVE:
|
||||
{
|
||||
// We need to call TrackMouseEvent in order to receive WM_MOUSELEAVE events
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
const int area = (msg == WM_MOUSEMOVE) ? 1 : 2;
|
||||
bd->MouseHwnd = hwnd;
|
||||
if (bd->MouseTrackedArea != area)
|
||||
{
|
||||
TRACKMOUSEEVENT tme_cancel = { sizeof(tme_cancel), TME_CANCEL, hwnd, 0 };
|
||||
TRACKMOUSEEVENT tme_track = { sizeof(tme_track), (DWORD)((area == 2) ? (TME_LEAVE | TME_NONCLIENT) : TME_LEAVE), hwnd, 0 };
|
||||
if (bd->MouseTrackedArea != 0)
|
||||
::TrackMouseEvent(&tme_cancel);
|
||||
::TrackMouseEvent(&tme_track);
|
||||
bd->MouseTrackedArea = area;
|
||||
}
|
||||
POINT mouse_pos = { (LONG)GET_X_LPARAM(lParam), (LONG)GET_Y_LPARAM(lParam) };
|
||||
if (msg == WM_NCMOUSEMOVE && ::ScreenToClient(hwnd, &mouse_pos) == FALSE) // WM_NCMOUSEMOVE are provided in absolute coordinates.
|
||||
return 0;
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMousePosEvent((float)mouse_pos.x, (float)mouse_pos.y);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSELEAVE:
|
||||
case WM_NCMOUSELEAVE:
|
||||
{
|
||||
const int area = (msg == WM_MOUSELEAVE) ? 1 : 2;
|
||||
if (bd->MouseTrackedArea == area)
|
||||
{
|
||||
if (bd->MouseHwnd == hwnd)
|
||||
bd->MouseHwnd = nullptr;
|
||||
bd->MouseTrackedArea = 0;
|
||||
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_DESTROY:
|
||||
if (bd->MouseHwnd == hwnd && bd->MouseTrackedArea != 0)
|
||||
{
|
||||
TRACKMOUSEEVENT tme_cancel = { sizeof(tme_cancel), TME_CANCEL, hwnd, 0 };
|
||||
::TrackMouseEvent(&tme_cancel);
|
||||
bd->MouseHwnd = nullptr;
|
||||
bd->MouseTrackedArea = 0;
|
||||
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
return 0;
|
||||
case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK:
|
||||
case WM_RBUTTONDOWN: case WM_RBUTTONDBLCLK:
|
||||
case WM_MBUTTONDOWN: case WM_MBUTTONDBLCLK:
|
||||
case WM_XBUTTONDOWN: case WM_XBUTTONDBLCLK:
|
||||
{
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
int button = 0;
|
||||
if (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONDBLCLK) { button = 0; }
|
||||
if (msg == WM_RBUTTONDOWN || msg == WM_RBUTTONDBLCLK) { button = 1; }
|
||||
if (msg == WM_MBUTTONDOWN || msg == WM_MBUTTONDBLCLK) { button = 2; }
|
||||
if (msg == WM_XBUTTONDOWN || msg == WM_XBUTTONDBLCLK) { button = (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) ? 3 : 4; }
|
||||
if (bd->MouseButtonsDown == 0 && ::GetCapture() == nullptr)
|
||||
::SetCapture(hwnd); // Allow us to read mouse coordinates when dragging mouse outside of our window bounds.
|
||||
bd->MouseButtonsDown |= 1 << button;
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMouseButtonEvent(button, true);
|
||||
return 0;
|
||||
}
|
||||
case WM_LBUTTONUP:
|
||||
case WM_RBUTTONUP:
|
||||
case WM_MBUTTONUP:
|
||||
case WM_XBUTTONUP:
|
||||
{
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
int button = 0;
|
||||
if (msg == WM_LBUTTONUP) { button = 0; }
|
||||
if (msg == WM_RBUTTONUP) { button = 1; }
|
||||
if (msg == WM_MBUTTONUP) { button = 2; }
|
||||
if (msg == WM_XBUTTONUP) { button = (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) ? 3 : 4; }
|
||||
bd->MouseButtonsDown &= ~(1 << button);
|
||||
if (bd->MouseButtonsDown == 0 && ::GetCapture() == hwnd)
|
||||
::ReleaseCapture();
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMouseButtonEvent(button, false);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSEWHEEL:
|
||||
io.AddMouseWheelEvent(0.0f, (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
|
||||
return 0;
|
||||
case WM_MOUSEHWHEEL:
|
||||
io.AddMouseWheelEvent(-(float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0.0f);
|
||||
return 0;
|
||||
case WM_KEYDOWN:
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYDOWN:
|
||||
case WM_SYSKEYUP:
|
||||
{
|
||||
const bool is_key_down = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
|
||||
if (wParam < 256)
|
||||
{
|
||||
// Submit modifiers
|
||||
ImGui_ImplWin32_UpdateKeyModifiers(io);
|
||||
|
||||
// Obtain virtual key code and convert to ImGuiKey
|
||||
const ImGuiKey key = ImGui_ImplWin32_KeyEventToImGuiKey(wParam, lParam);
|
||||
const int vk = (int)wParam;
|
||||
const int scancode = (int)LOBYTE(HIWORD(lParam));
|
||||
|
||||
// Special behavior for VK_SNAPSHOT / ImGuiKey_PrintScreen as Windows doesn't emit the key down event.
|
||||
if (key == ImGuiKey_PrintScreen && !is_key_down)
|
||||
ImGui_ImplWin32_AddKeyEvent(io, key, true, vk, scancode);
|
||||
|
||||
// Submit key event
|
||||
if (key != ImGuiKey_None)
|
||||
ImGui_ImplWin32_AddKeyEvent(io, key, is_key_down, vk, scancode);
|
||||
|
||||
// Submit individual left/right modifier events
|
||||
if (vk == VK_SHIFT)
|
||||
{
|
||||
// Important: Shift keys tend to get stuck when pressed together, missing key-up events are corrected in ImGui_ImplWin32_ProcessKeyEventsWorkarounds()
|
||||
if (IsVkDown(VK_LSHIFT) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftShift, is_key_down, VK_LSHIFT, scancode); }
|
||||
if (IsVkDown(VK_RSHIFT) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightShift, is_key_down, VK_RSHIFT, scancode); }
|
||||
}
|
||||
else if (vk == VK_CONTROL)
|
||||
{
|
||||
if (IsVkDown(VK_LCONTROL) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftCtrl, is_key_down, VK_LCONTROL, scancode); }
|
||||
if (IsVkDown(VK_RCONTROL) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightCtrl, is_key_down, VK_RCONTROL, scancode); }
|
||||
}
|
||||
else if (vk == VK_MENU)
|
||||
{
|
||||
if (IsVkDown(VK_LMENU) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftAlt, is_key_down, VK_LMENU, scancode); }
|
||||
if (IsVkDown(VK_RMENU) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightAlt, is_key_down, VK_RMENU, scancode); }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_SETFOCUS:
|
||||
case WM_KILLFOCUS:
|
||||
io.AddFocusEvent(msg == WM_SETFOCUS);
|
||||
return 0;
|
||||
case WM_INPUTLANGCHANGE:
|
||||
ImGui_ImplWin32_UpdateKeyboardCodePage(io);
|
||||
return 0;
|
||||
case WM_CHAR:
|
||||
if (::IsWindowUnicode(hwnd))
|
||||
{
|
||||
// You can also use ToAscii()+GetKeyboardState() to retrieve characters.
|
||||
if (wParam > 0 && wParam < 0x10000)
|
||||
io.AddInputCharacterUTF16((unsigned short)wParam);
|
||||
}
|
||||
else
|
||||
{
|
||||
wchar_t wch = 0;
|
||||
::MultiByteToWideChar(bd->KeyboardCodePage, MB_PRECOMPOSED, (char*)&wParam, 1, &wch, 1);
|
||||
io.AddInputCharacter(wch);
|
||||
}
|
||||
return 0;
|
||||
case WM_SETCURSOR:
|
||||
// This is required to restore cursor when transitioning from e.g resize borders to client area.
|
||||
if (LOWORD(lParam) == HTCLIENT && ImGui_ImplWin32_UpdateMouseCursor(io, bd->LastMouseCursor))
|
||||
return 1;
|
||||
return 0;
|
||||
case WM_DEVICECHANGE:
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
if ((UINT)wParam == DBT_DEVNODES_CHANGED)
|
||||
bd->WantUpdateHasGamepad = true;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
// DPI-related helpers (optional)
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
// - Use to enable DPI awareness without having to create an application manifest.
|
||||
// - Your own app may already do this via a manifest or explicit calls. This is mostly useful for our examples/ apps.
|
||||
// - In theory we could call simple functions from Windows SDK such as SetProcessDPIAware(), SetProcessDpiAwareness(), etc.
|
||||
// but most of the functions provided by Microsoft require Windows 8.1/10+ SDK at compile time and Windows 8/10+ at runtime,
|
||||
// neither we want to require the user to have. So we dynamically select and load those functions to avoid dependencies.
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// This is the scheme successfully used by GLFW (from which we borrowed some of the code) and other apps aiming to be highly portable.
|
||||
// ImGui_ImplWin32_EnableDpiAwareness() is just a helper called by main.cpp, we don't call it automatically.
|
||||
// If you are trying to implement your own backend for your own engine, you may ignore that noise.
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Perform our own check with RtlVerifyVersionInfo() instead of using functions from <VersionHelpers.h> as they
|
||||
// require a manifest to be functional for checks above 8.1. See https://github.com/ocornut/imgui/issues/4200
|
||||
static BOOL _IsWindowsVersionOrGreater(WORD major, WORD minor, WORD)
|
||||
{
|
||||
typedef LONG(WINAPI* PFN_RtlVerifyVersionInfo)(OSVERSIONINFOEXW*, ULONG, ULONGLONG);
|
||||
static PFN_RtlVerifyVersionInfo RtlVerifyVersionInfoFn = nullptr;
|
||||
if (RtlVerifyVersionInfoFn == nullptr)
|
||||
if (HMODULE ntdllModule = ::GetModuleHandleA("ntdll.dll"))
|
||||
RtlVerifyVersionInfoFn = (PFN_RtlVerifyVersionInfo)GetProcAddress(ntdllModule, "RtlVerifyVersionInfo");
|
||||
if (RtlVerifyVersionInfoFn == nullptr)
|
||||
return FALSE;
|
||||
|
||||
RTL_OSVERSIONINFOEXW versionInfo = { };
|
||||
ULONGLONG conditionMask = 0;
|
||||
versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);
|
||||
versionInfo.dwMajorVersion = major;
|
||||
versionInfo.dwMinorVersion = minor;
|
||||
VER_SET_CONDITION(conditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL);
|
||||
VER_SET_CONDITION(conditionMask, VER_MINORVERSION, VER_GREATER_EQUAL);
|
||||
return (RtlVerifyVersionInfoFn(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION, conditionMask) == 0) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
#define _IsWindowsVistaOrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0600), LOBYTE(0x0600), 0) // _WIN32_WINNT_VISTA
|
||||
#define _IsWindows8OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0602), LOBYTE(0x0602), 0) // _WIN32_WINNT_WIN8
|
||||
#define _IsWindows8Point1OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0603), LOBYTE(0x0603), 0) // _WIN32_WINNT_WINBLUE
|
||||
#define _IsWindows10OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0A00), LOBYTE(0x0A00), 0) // _WIN32_WINNT_WINTHRESHOLD / _WIN32_WINNT_WIN10
|
||||
|
||||
#ifndef DPI_ENUMS_DECLARED
|
||||
typedef enum { PROCESS_DPI_UNAWARE = 0, PROCESS_SYSTEM_DPI_AWARE = 1, PROCESS_PER_MONITOR_DPI_AWARE = 2 } PROCESS_DPI_AWARENESS;
|
||||
typedef enum { MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2, MDT_DEFAULT = MDT_EFFECTIVE_DPI } MONITOR_DPI_TYPE;
|
||||
#endif
|
||||
#ifndef _DPI_AWARENESS_CONTEXTS_
|
||||
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
|
||||
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE (DPI_AWARENESS_CONTEXT)-3
|
||||
#endif
|
||||
#ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
|
||||
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (DPI_AWARENESS_CONTEXT)-4
|
||||
#endif
|
||||
typedef HRESULT(WINAPI* PFN_SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS); // Shcore.lib + dll, Windows 8.1+
|
||||
typedef HRESULT(WINAPI* PFN_GetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*); // Shcore.lib + dll, Windows 8.1+
|
||||
typedef DPI_AWARENESS_CONTEXT(WINAPI* PFN_SetThreadDpiAwarenessContext)(DPI_AWARENESS_CONTEXT); // User32.lib + dll, Windows 10 v1607+ (Creators Update)
|
||||
|
||||
// Helper function to enable DPI awareness without setting up a manifest
|
||||
void ImGui_ImplWin32_EnableDpiAwareness()
|
||||
{
|
||||
if (_IsWindows10OrGreater())
|
||||
{
|
||||
static HINSTANCE user32_dll = ::LoadLibraryA("user32.dll"); // Reference counted per-process
|
||||
if (PFN_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContextFn = (PFN_SetThreadDpiAwarenessContext)::GetProcAddress(user32_dll, "SetThreadDpiAwarenessContext"))
|
||||
{
|
||||
SetThreadDpiAwarenessContextFn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_IsWindows8Point1OrGreater())
|
||||
{
|
||||
static HINSTANCE shcore_dll = ::LoadLibraryA("shcore.dll"); // Reference counted per-process
|
||||
if (PFN_SetProcessDpiAwareness SetProcessDpiAwarenessFn = (PFN_SetProcessDpiAwareness)::GetProcAddress(shcore_dll, "SetProcessDpiAwareness"))
|
||||
{
|
||||
SetProcessDpiAwarenessFn(PROCESS_PER_MONITOR_DPI_AWARE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#if _WIN32_WINNT >= 0x0600
|
||||
::SetProcessDPIAware();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(_MSC_VER) && !defined(NOGDI)
|
||||
#pragma comment(lib, "gdi32") // Link with gdi32.lib for GetDeviceCaps(). MinGW will require linking with '-lgdi32'
|
||||
#endif
|
||||
|
||||
float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor)
|
||||
{
|
||||
UINT xdpi = 96, ydpi = 96;
|
||||
if (_IsWindows8Point1OrGreater())
|
||||
{
|
||||
static HINSTANCE shcore_dll = ::LoadLibraryA("shcore.dll"); // Reference counted per-process
|
||||
static PFN_GetDpiForMonitor GetDpiForMonitorFn = nullptr;
|
||||
if (GetDpiForMonitorFn == nullptr && shcore_dll != nullptr)
|
||||
GetDpiForMonitorFn = (PFN_GetDpiForMonitor)::GetProcAddress(shcore_dll, "GetDpiForMonitor");
|
||||
if (GetDpiForMonitorFn != nullptr)
|
||||
{
|
||||
GetDpiForMonitorFn((HMONITOR)monitor, MDT_EFFECTIVE_DPI, &xdpi, &ydpi);
|
||||
IM_ASSERT(xdpi == ydpi); // Please contact me if you hit this assert!
|
||||
return xdpi / 96.0f;
|
||||
}
|
||||
}
|
||||
#ifndef NOGDI
|
||||
const HDC dc = ::GetDC(nullptr);
|
||||
xdpi = ::GetDeviceCaps(dc, LOGPIXELSX);
|
||||
ydpi = ::GetDeviceCaps(dc, LOGPIXELSY);
|
||||
IM_ASSERT(xdpi == ydpi); // Please contact me if you hit this assert!
|
||||
::ReleaseDC(nullptr, dc);
|
||||
#endif
|
||||
return xdpi / 96.0f;
|
||||
}
|
||||
|
||||
float ImGui_ImplWin32_GetDpiScaleForHwnd(void* hwnd)
|
||||
{
|
||||
HMONITOR monitor = ::MonitorFromWindow((HWND)hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
return ImGui_ImplWin32_GetDpiScaleForMonitor(monitor);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// Transparency related helpers (optional)
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#pragma comment(lib, "dwmapi") // Link with dwmapi.lib. MinGW will require linking with '-ldwmapi'
|
||||
#endif
|
||||
|
||||
// [experimental]
|
||||
// Borrowed from GLFW's function updateFramebufferTransparency() in src/win32_window.c
|
||||
// (the Dwm* functions are Vista era functions but we are borrowing logic from GLFW)
|
||||
void ImGui_ImplWin32_EnableAlphaCompositing(void* hwnd)
|
||||
{
|
||||
if (!_IsWindowsVistaOrGreater())
|
||||
return;
|
||||
|
||||
BOOL composition;
|
||||
if (FAILED(::DwmIsCompositionEnabled(&composition)) || !composition)
|
||||
return;
|
||||
|
||||
BOOL opaque;
|
||||
DWORD color;
|
||||
if (_IsWindows8OrGreater() || (SUCCEEDED(::DwmGetColorizationColor(&color, &opaque)) && !opaque))
|
||||
{
|
||||
HRGN region = ::CreateRectRgn(0, 0, -1, -1);
|
||||
DWM_BLURBEHIND bb = {};
|
||||
bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
|
||||
bb.hRgnBlur = region;
|
||||
bb.fEnable = TRUE;
|
||||
::DwmEnableBlurBehindWindow((HWND)hwnd, &bb);
|
||||
::DeleteObject(region);
|
||||
}
|
||||
else
|
||||
{
|
||||
DWM_BLURBEHIND bb = {};
|
||||
bb.dwFlags = DWM_BB_ENABLE;
|
||||
::DwmEnableBlurBehindWindow((HWND)hwnd, &bb);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
|
||||
#if defined(__GNUC__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
#endif
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
// dear imgui: Platform Backend for Windows (standard windows API for 32-bits AND 64-bits applications)
|
||||
// This needs to be used along with a Renderer (e.g. DirectX11, OpenGL3, Vulkan..)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Platform: Clipboard support (for Win32 this is actually part of core dear imgui)
|
||||
// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen.
|
||||
// [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy VK_* values are obsolete since 1.87 and not supported since 1.91.5]
|
||||
// [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
|
||||
// [X] Platform: Mouse cursor shape and visibility (ImGuiBackendFlags_HasMouseCursors). Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_IMPL_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_Init(void* hwnd);
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_InitForOpenGL(void* hwnd);
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_Shutdown();
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_NewFrame();
|
||||
|
||||
// Win32 message handler your application need to call.
|
||||
// - Intentionally commented out in a '#if 0' block to avoid dragging dependencies on <windows.h> from this helper.
|
||||
// - You should COPY the line below into your .cpp code to forward declare the function and then you can call it.
|
||||
// - Call from your application's message handler. Keep calling your message handler unless this function returns TRUE.
|
||||
|
||||
#if 0
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
#endif
|
||||
|
||||
// DPI-related helpers (optional)
|
||||
// - Use to enable DPI awareness without having to create an application manifest.
|
||||
// - Your own app may already do this via a manifest or explicit calls. This is mostly useful for our examples/ apps.
|
||||
// - In theory we could call simple functions from Windows SDK such as SetProcessDPIAware(), SetProcessDpiAwareness(), etc.
|
||||
// but most of the functions provided by Microsoft require Windows 8.1/10+ SDK at compile time and Windows 8/10+ at runtime,
|
||||
// neither we want to require the user to have. So we dynamically select and load those functions to avoid dependencies.
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_EnableDpiAwareness();
|
||||
IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForHwnd(void* hwnd); // HWND hwnd
|
||||
IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor); // HMONITOR monitor
|
||||
|
||||
// Transparency related helpers (optional) [experimental]
|
||||
// - Use to enable alpha compositing transparency with the desktop.
|
||||
// - Use together with e.g. clearing your framebuffer with zero-alpha.
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_EnableAlphaCompositing(void* hwnd); // HWND hwnd
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
Vendored
+3614
File diff suppressed because it is too large.
Load diff
Vendored
+4501
File diff suppressed because it is too large.
Load diff
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user