Flick the bare hands for a trick

- A bare hand already feeds no Wii Remote motion (camera-tracked poses are too noisy to
  differentiate twice). In a cockpit race, both hands on the wheel rising together or a free bare
  hand rising fast now plays one shake on the remote's accelerometer: 150 ms, so the guest sees it
  on at least three frames, one cycle up to +2 g and down to the -3.6 g limit, as Dolphin's
  emulated shake does. A turn (one hand up, the other down) never flicks, nor does a lone hand on
  the wheel; a rise must last three samples and cover 5 cm within 150 ms, tracking jumps reset it,
  and flicks are 0.5 s apart. Wii Remote presentation only.
- debug.wiicompiled.inject <n>:flick plays the same shake, with the controllers or unattended, to
  tune it apart from the gesture. Docs: OPENXR.md, docs/quest-port.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
iChris4andClaude Opus 5.5 committed 2026-09-25 00:10:16 +02:00
1 parent 5a38377f61
commit 771a143254
6 files changed
+59 -11

No files matched your search

+17 -4
View File
@@ -279,8 +279,8 @@ own motion detection still reads them, so a sharp enough turn can read as a shak
`khr/simple_controller`: a right pinch is A with the pointer on the hand's aim ray, the left
palm-up pinch is + (pause), and in a cockpit race a hand on the wheel holds A and a free hand's
pinch is Z; see "Tracked hands" under [Steering wheel and hand
steering](#steering-wheel-and-hand-steering). A bare hand feeds no motion. With `hand_tracking` off
a bare hand presses nothing but +.
steering](#steering-wheel-and-hand-steering). A bare hand feeds no motion; flicking the hands up
plays one shake instead. With `hand_tracking` off a bare hand presses nothing but +.
`"gamepad"` keeps the controllers one ordinary gamepad read through PAD as a GameCube controller:
A/B → South/East, X/Y → West/North, index triggers → trigger axes, grips → shoulders, thumbsticks
@@ -556,6 +556,17 @@ presentation there is no pointer, so the race controls apply throughout the cock
has no gesture: choose Automatic drift. The headset panel reads out each hand's source, grasp,
hold and pinch under the checkbox, for tuning.
A bare hand feeds no Wii Remote motion: camera-tracked poses are too noisy to differentiate twice,
and turning the wheel would trick and wheelie. Tricks come from a flick instead. Both hands on
the wheel rising together (at least 1.2 m/s on average, 0.6 m/s each, within 0.6 m/s of each
other) or a free bare hand rising at 1.5 m/s makes one; a turn, where one hand rises as the other
drops, never does, nor does a lone hand on the wheel. A rise has to last three samples and cover
5 cm within 150 ms, a pose jumping faster than 5 m/s (tracking coming back) resets it, and
flicks are 0.5 s apart. A flick plays one shake on the remote's accelerometer: 150 ms, so the
guest sees it on at least three of its frames, one cycle up to +2 g and down to the -3.6 g limit,
as Dolphin's emulated shake does. Only the Wii Remote presentation has it (the gamepad has no
shake); `debug.wiicompiled.inject <n>:flick` plays the same shake with or without hands.
**Hands and the separate wheel.** Hands are drawn while hand steering is on: the runtime's own hand
mesh where it offers one (`XR_EXT_hand_tracking` and `XR_FB_hand_tracking_mesh`, requested when
hand steering or tracked hands are on at launch), otherwise procedural gloves that curl with the
@@ -579,8 +590,10 @@ The guest offsets involved (driver, movement, damage, grip frames, bike handle,
their world matrices) are PAL `RMCP01` constants listed with the leaf getter or constructor that
proves each in `runtime/src/vr/mkw_vr_first_person.cpp`. `mkw_steering_wheel_tests`,
`mkw_vr_cockpit_tests` and `mkw_vr_hand_steering_tests` cover the grab model, the seat and wheel
geometry and the hand-off to the game; `gx_fifo_tests` covers the per-draw substitution and the
overlay geometry, and `cockpit_gpu_smoke` its depth test on a real GPU.
geometry and the hand-off to the game, and `mkw_vr_hand_tracking_tests` the tracked hands' rules
(grasp, bare latch, pinch gate, bare-hand buttons, flick and its shake); `gx_fifo_tests` covers the
per-draw substitution and the overlay geometry, joint skinning included, and `cockpit_gpu_smoke`
its depth test on a real GPU.
## Presentation policy
+1 -1
View File
@@ -534,7 +534,7 @@
<string name="vr_hand_steering">Hand steering</string>
<string name="vr_hand_steering_helper">In the cockpit, squeeze a grip near the steering wheel or handlebar to grab it, and turn it to steer. Releasing both grips gives steering back to the stick. Hand steering by heurazy.</string>
<string name="vr_hand_tracking">Tracked hands</string>
<string name="vr_hand_tracking_helper">The cockpit hands follow your own. Holding the controllers, the fingers follow their touch sensors. Put the controllers down to drive with bare hands: close a hand on the wheel to hold it, which also holds the gas; pinch with a free hand to use an item; pinch with your left palm facing you to pause. In menus, a right pinch is A. Choose Automatic drift. Needs hand tracking on in the headset\'s settings.</string>
<string name="vr_hand_tracking_helper">The cockpit hands follow your own. Holding the controllers, the fingers follow their touch sensors. Put the controllers down to drive with bare hands: close a hand on the wheel to hold it, which also holds the gas; pinch with a free hand to use an item; flick your hands up for a trick; pinch with your left palm facing you to pause. In menus, a right pinch is A. Choose Automatic drift. Needs hand tracking on in the headset\'s settings.</string>
<string name="vr_lean_back">Lean back angle</string>
<string name="vr_lean_back_helper">Tilts the race view back for playing reclined. 0 applies no tilt.</string>
+1 -1
View File
@@ -921,7 +921,7 @@ the app:
| `debug.wiicompiled.eye_passes 0` | Replays each eye in one render pass per recorded pass, as before `single_pass_eyes` (`OPENXR.md`); `1` forces the single pass and an empty value restores the setting. Read about once a second, for A/B timing inside one session |
| `debug.wiicompiled.foveation <0-3>` | Overrides the foveation level (off, low, medium, high) within one session; an empty value restores the setting. Needs a session launched with foveation on. Read about once a second |
| `debug.wiicompiled.fdm 0` | Launches without fragment density maps at all, whatever `foveation` says, which also drops their flag from every pipeline; `1` asks for them even with `foveation = "off"` |
| `debug.wiicompiled.inject <n>:<button>` | Presses `a`, `b`, `x`, `y`, `start`, `up`, `down`, `left` or `right` for 12 XR frames each time `<n>` changes. As a Wii Remote, `x`/`y`/`start` are 1/2/+, the directions push the Nunchuk stick, and `home`, `c` and `z` also exist. `panel` presses the settings panel's button (left Y, or both thumbsticks as a gamepad), opening or closing it (see `OPENXR.md`) |
| `debug.wiicompiled.inject <n>:<button>` | Presses `a`, `b`, `x`, `y`, `start`, `up`, `down`, `left` or `right` for 12 XR frames each time `<n>` changes. As a Wii Remote, `x`/`y`/`start` are 1/2/+, the directions push the Nunchuk stick, and `home`, `c` and `z` also exist. `panel` presses the settings panel's button (left Y, or both thumbsticks as a gamepad), opening or closing it (see `OPENXR.md`). `flick` plays the bare hands' flick, one 150 ms shake of the remote (a trick off a ramp, a wheelie on a bike), with the controllers or none |
| `debug.wiicompiled.fpslog 1` | Logs the game's rendered frame rate every 5 s, with per-frame averages of the producer's waits for the frame worker's DONE and SEALED phases and of the worker's seal, permit wait, prepare and encode stretches, and of the draw calls the recorded frame holds and the primitives that merged into them (an overlay that stops draws merging shows up there first). A third line reports the GX thread's command ring (records, waits, busy share). A second line gives the GPU time per frame from timestamp queries on every pass (`mono` native render, `eyeL`/`eyeR` replays, `screen`, `panel`, `efbcopy`, `palette`, `peek`, plus `passes-span` from the first pass begin to the last pass end and `between-passes` for copies and idle gaps). The compositor's `VrApi` log line gives headset FPS, `GPU%`, `CPU%`, clock levels and app GPU time (`App=`) |
A `Config.toml` written with `adb push` (or `sed -i` in `adb shell`) belongs
+5
View File
@@ -242,6 +242,11 @@ private:
std::array<hand_tracking::BareLatch, kHands> m_bare_latch{};
std::array<hand_tracking::PinchGate, kHands> m_pinch_gate{};
int m_logged_game_pointer = -2;
// A flick of the bare hands plays one shake on the remote's accelerometer
// from this input time (0 when none is playing).
hand_tracking::FlickDetector m_flick;
XrTime m_flick_start = 0;
bool m_injected_flick_held = false;
uint64_t m_profile_serial = 0;
bool m_created = false;
+3 -3
View File
@@ -1194,9 +1194,9 @@ void DrawVrSteeringWheelSettings() {
? "The cockpit hands follow your own. Holding the controllers, the fingers follow "
"their touch sensors. Put the controllers down to drive with bare hands: close a "
"hand on the wheel to hold it, which also holds the gas; pinch with a free hand "
"to use an item; pinch with your left palm facing you to pause. In menus, a "
"right pinch is A. Choose Automatic drift. Needs hand tracking on in the "
"headset's settings."
"to use an item; flick your hands up for a trick; pinch with your left palm "
"facing you to pause. In menus, a right pinch is A. Choose Automatic drift. "
"Needs hand tracking on in the headset's settings."
: "The cockpit hands follow your own, from the controllers' touch sensors or, "
"with the controllers put down, the headset's cameras. This session started "
"with hand steering and tracked hands off, so it applies after a restart.");
+32 -2
View File
@@ -124,8 +124,9 @@ VirtualGamepadRelay& Relay() {
// presentation also home, c and z (x/y/start press 1/2/+ there, and the
// directions push the Nunchuk stick). `panel` presses the settings panel's
// button (left Y, or both thumbsticks for a gamepad), opening or closing it,
// where `a` then selects. The property is unset in normal use, so this costs
// one property read every few frames.
// where `a` then selects. `flick` plays the bare hands' flick, one shake of the
// remote. The property is unset in normal use, so this costs one property read
// every few frames.
constexpr uint32_t kInjectHoldFrames = 12;
constexpr uint32_t kInjectPollFrames = 4;
@@ -745,6 +746,7 @@ void OpenXRInput::Destroy() {
m_logged_sources = {};
m_squeeze_active = m_hand_driven = m_pinch = {};
m_logged_game_pointer = -2;
m_injected_flick_held = false;
m_profile_serial = 0;
DestroyPoseSpaces();
if (m_action_set != XR_NULL_HANDLE) {
@@ -987,17 +989,32 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
!panel.withheld && game_pointer <= 0;
if (bare_race) {
std::array<bool, kHands> bare_held{}, item_pinch{};
std::array<hand_tracking::FlickHand, kHands> flick{};
for (uint32_t hand = 0; hand < kHands; ++hand) {
bare_held[hand] = m_bare_latch[hand].Bare() && m_wheel_held[hand];
item_pinch[hand] = m_hand_driven[hand] && m_pinch_gate[hand].Update(m_pinch[hand], m_wheel_held[hand],
dt_seconds);
flick[hand] = {m_bare_latch[hand].Tracked(), m_wheel_held[hand],
m_joint_frame.seat_from_joint[hand][hand_tracking::kPalm][7]};
}
hand_tracking::ApplyBareHandRace(hands, m_hand_driven, bare_held, item_pinch);
// A trick or a wheelie: the remote's shake, which the gamepad cannot give.
if (m_flick.Update(flick, dt_seconds) && wii_remote) {
m_flick_start = input_time;
}
} else {
for (auto& gate : m_pinch_gate) {
gate.Reset();
}
m_flick.Reset();
}
// `debug.wiicompiled.inject <n>:flick` plays the same shake, with the
// controllers or unattended, to tune it apart from the gesture.
const bool injected_flick = Injected("flick");
if (injected_flick && !m_injected_flick_held) {
m_flick_start = input_time;
}
m_injected_flick_held = injected_flick;
#endif
// While the panel has the controllers, the game sees them idle.
@@ -1130,9 +1147,20 @@ void OpenXRInput::PublishWiiRemote(XrTime input_time, const OpenXRPointerScreen&
sample.acc = OpenXRWiiRemoteSample{}.acc;
sample.nunchuk_acc = OpenXRWiiRemoteSample{}.nunchuk_acc;
m_pointer.Reset();
m_flick_start = 0;
OpenXRPublishWiiRemote(m_joystick_id, sample);
return;
}
if (m_flick_start != 0) {
// A bare-hand flick: one clean shake, whatever the hands' own motion.
bool playing = false;
const wii_remote::Vec3 acc = hand_tracking::FlickPulse(input_time - m_flick_start, &playing);
if (playing) {
sample.acc = acc;
} else {
m_flick_start = 0;
}
}
wii_remote::Screen target{};
wii_remote::ScreenHit hit{};
@@ -1173,6 +1201,8 @@ void OpenXRInput::ResetDriving() {
m_bare_latch[hand].Reset();
m_pinch_gate[hand].Reset();
}
m_flick.Reset();
m_flick_start = 0;
OpenXRPublishDriving(m_driving);
}