Files
mitch030504--Wiicompiled_VR…/runtime/include/vr/steering_wheel.h
T
iChris4 50dc354123 Add the cockpit, steering-wheel and hand-steering building blocks
The pure pieces of the first-person cockpit and hand steering, ported from
heurazy's mario-kart-wii-VR-port: the SteeringWheel grab/turn model, the
native wheel vertex rotation, the level seat stabiliser, the seated-eye and
wheel/handlebar geometry, and the XR_FB_hand_tracking_mesh loader. Adds
openxr_driving.h, the OpenXR-free snapshot the pacing thread will publish for
the guest thread, with the hand-off rule (a held wheel replaces the left
stick's X and releases that hand's grip for the game) and the wheel's
displayed angle.

New [vr] keys: first_person_seat (cockpit), cockpit_units_per_meter (100),
steering_wheel (true), native_steering_wheel (true), hand_steering (false)
and the seven wheel_* tuning keys. Nothing reads them yet.

The fresh-config template now writes the first-person defaults the
constants hold (50 / 1.5 / 0); d86dcb0 updated the constants but not the
template.

Tests: mkw_steering_wheel_tests (the fork's), mkw_vr_cockpit_tests,
mkw_vr_hand_steering_tests.
2026-09-22 03:55:40 +02:00

164 lines
8.2 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Ported from heurazy's mario-kart-wii-VR-port (GPL-3.0-or-later).
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <cstdint>
namespace mkw::vr {
// Metres in a fixed seated frame: +X right, +Y up, -Z forward.
struct WheelHand { float x=0, y=0, z=0, squeeze=0; bool tracked=false; };
struct WheelGeometry {
std::array<float,3> center{}, right{1,0,0}, up{0,1,0}, normal{0,0,1};
float radius=0.18f;
bool valid=false;
WheelHand ToWheel(WheelHand hand) const {
const std::array<float,3> p{hand.x-center[0],hand.y-center[1],hand.z-center[2]};
const auto dot=[&](const auto& axis) { return p[0]*axis[0]+p[1]*axis[1]+p[2]*axis[2]; };
hand.x=dot(right); hand.y=dot(up)-0.30f; hand.z=dot(normal)-0.42f;
return hand;
}
};
struct WheelState {
float angle=0, steering=0, visualAngle=0;
std::array<bool, 2> held{};
};
class WheelReferenceLatch {
WheelGeometry last_{};
uint64_t identity_=0;
float missing_=0;
bool bike_=false;
public:
bool Resolve(WheelGeometry& geometry, bool enabled, bool available, bool held,
bool bike, uint64_t identity, float dt) {
if(!enabled || identity_!=identity || bike_!=bike) { last_={};missing_=0; }
identity_=identity;bike_=bike;
if(!enabled) return false;
if(available && geometry.valid) { last_=geometry;missing_=0;return true; }
missing_+=std::clamp(dt,0.0f,0.05f);
if(held && last_.valid && missing_<0.20f) { geometry=last_;return true; }
last_={};return false;
}
};
struct WheelTuning {
float kartDegrees=90, bikeDegrees=45, grabDistance=0.35f, grabAssist=1, response=1, trackingGrace=0.20f;
bool haptics=true;
};
class SteeringWheel {
public:
static constexpr float Radius=0.18f, Height=-0.30f, Depth=-0.42f;
WheelState Update(const std::array<WheelHand, 2>& hands, bool active, float dt, float radius=Radius, bool handlebars=false,
WheelTuning tuning={}) {
const auto finite=[](float value) { uint32_t bits; std::memcpy(&bits,&value,sizeof(bits)); return (bits&0x7f800000u)!=0x7f800000u; };
if (!finite(dt)) dt=0;
if (!finite(radius) || radius<0.04f || radius>1.0f) { active=false; radius=Radius; }
dt=std::clamp(dt, 0.0f, 0.05f);
const bool previouslyHeld=state_.held[0]||state_.held[1];
float deltaSum=0,weightSum=0;
std::array<bool,2> moving{};
std::array<bool,2> validHands{};
std::array<float,2> delta{};
const float degrees=handlebars?tuning.bikeDegrees:tuning.kartDegrees;
const float maxAngle=(finite(degrees)?std::clamp(degrees,20.0f,180.0f):(handlebars?45.0f:90.0f))*0.01745329252f;
const float assist=finite(tuning.grabAssist)?std::clamp(tuning.grabAssist,0.7f,2.0f):1.0f;
const float reach=finite(tuning.grabDistance)?std::clamp(tuning.grabDistance,0.15f,0.8f):0.35f;
const float grace=finite(tuning.trackingGrace)?std::clamp(tuning.trackingGrace,0.05f,0.5f):0.2f;
for (int h=0; h<2; ++h) {
const auto& p=hands[h];
const bool valid=p.tracked&&finite(p.x)&&finite(p.y)&&finite(p.z)&&finite(p.squeeze);
validHands[h]=valid;
const bool down=finite(p.squeeze)&&p.squeeze > (pressed_[h] ? 0.15f : 0.55f);
if(!valid) {
lost_[h]+=dt;
if(!p.tracked && down && active && state_.held[h] && lost_[h]<grace) {
center_[h]=true;
} else { state_.held[h]=false; pressed_[h]=down; }
continue;
}
lost_[h]=0;
const float y=p.y-Height, z=p.z-Depth;
const float radial=std::hypot(p.x,y);
const float gripX=radius*std::cos(state_.angle),gripY=-radius*std::sin(state_.angle);
const bool near_rim=std::abs(z)<reach && (handlebars
? std::min(std::hypot(p.x-gripX,y-gripY),std::hypot(p.x+gripX,y+gripY))<std::max(0.22f,radius*0.55f)*assist
: radial<std::max(radius+0.16f,0.32f)*assist);
const float angle=-std::atan2(y,p.x);
// Arcade latch: distance only gates acquisition. Once grabbed,
// large gestures and vehicle animation cannot release ownership.
if (!active || !down)
state_.held[h]=false;
else if (!pressed_[h] && near_rim) {
state_.held[h]=true;
last_[h]=angle;
center_[h]=false;
}
if (state_.held[h]) {
// Angle is undefined at the hub. Keep ownership and the last
// steering value, then rebase on exit to avoid a 180-degree jump.
if (radial<(center_[h]?0.065f:0.045f)) center_[h]=true;
else {
moving[h]=!center_[h];
if (moving[h]) delta[h]=std::remainder(angle-last_[h], 6.283185307f);
last_[h]=angle;
center_[h]=false;
}
if(moving[h]) {
const float weight=std::clamp(radial/0.18f,0.15f,1.0f);
deltaSum+=delta[h]*weight; weightSum+=weight;
}
}
pressed_[h]=down;
}
// Two hands define one rigid control. Their relative angle ignores
// shared translations, so leaning or moving both arms does not steer.
const float spanX=hands[1].x-hands[0].x,spanY=hands[1].y-hands[0].y;
// Pair orientation is defined by the span, even when one hand is near
// the original hub after a common translation of both arms.
const bool pair=state_.held[0]&&state_.held[1]&&validHands[0]&&validHands[1]&&
std::hypot(spanX,spanY)>(pairValid_?0.10f:0.14f);
const float pairAngle=pair ? -std::atan2(spanY,spanX):0;
float change=weightSum>0 ? deltaSum/weightSum:0;
// With both hands held, do not switch to angles about the hub when
// their span collapses: that changes the reference frame and creates
// false turns as the hands approach/cross each other. Hold the angle
// through that singularity and establish a fresh pair baseline on exit.
if(state_.held[0] && state_.held[1])
change=pair && pairValid_ ? std::remainder(pairAngle-lastPair_,6.283185307f):0;
pairValid_=pair; lastPair_=pairAngle;
// Rebase a discontinuous tracking pose without sending a full-lock
// impulse. Normal fast arcade steering remains inside this envelope.
if(std::abs(change)>0.25f+8.0f*dt) change=0;
const bool held=state_.held[0]||state_.held[1];
if(held && !previouslyHeld) target_=state_.angle;
// A single accumulated target avoids jumps when a second hand joins,
// leaves, passes through the hub or temporarily loses tracking.
// Keep physical overtravel. Clamping this accumulator discards motion
// past full lock, so retracing the gesture no longer returns to centre.
// Only the game's steering command is saturated, never the hand angle.
target_=held ? target_+change:0;
const float error=target_-state_.angle;
// Quiet near a steady heading, responsive during deliberate turns.
const float tuningResponse=finite(tuning.response)?std::clamp(tuning.response,0.5f,2.0f):1;
const float response=held ? std::clamp((18.0f+80.0f*std::abs(error))*tuningResponse,9.0f,90.0f):12.0f;
state_.angle += error*(1-std::exp(-dt*response));
state_.steering=active ? std::clamp(state_.angle/maxAngle,-1.0f,1.0f) : 0;
if (!active) { state_.angle=0;target_=0;pairValid_=false; }
// Share the validated physical rotation with both renderer paths.
// Handlebars keep their limited travel; a kart wheel can turn freely.
state_.visualAngle=handlebars ? std::clamp(state_.angle,-maxAngle,maxAngle)
: std::remainder(state_.angle,6.283185307f);
return state_;
}
private:
WheelState state_{};
std::array<bool,2> pressed_{};
std::array<bool,2> center_{};
std::array<float,2> last_{};
std::array<float,2> lost_{};
float target_=0,lastPair_=0;
bool pairValid_=false;
};
} // namespace mkw::vr