Add placeholder steering wheel option for VR driving

This commit is contained in:
iChris4 committed 2026-09-30 02:40:36 +02:00
1 parent ebc97ef9ce
commit 85fab2fa09
10 files changed
+101 -22

No files matched your search

+11 -3
View File
@@ -60,6 +60,7 @@ first_person_hidden_model = 0
first_person_rotation = "yaw_pitch"
steering_wheel = true
native_steering_wheel = true
placeholder_steering_wheel = false
object_culling = false
hand_steering = true
cockpit_item_hand = "left"
@@ -599,9 +600,16 @@ guest's own vertices are never written, and the copies are dropped after the fra
turn their handle part in the game already; its copy is only re-seated on the cockpit frame so the
bars stay with your hands while the bike banks. The wheel rides in the same frame as the view: the
level seat for `"yaw"`, the kart's own orientation for `"yaw_pitch"` and `"full"`. While no draw takes
the copy (for 30 frames running; the race's opening pan does this) a separate VR wheel stands in,
which is also what `native_steering_wheel = false` draws. The copy keeps being published, so the
vehicle's own wheel returns as soon as draws take it again, and the log notes both switches.
the copy (for 30 frames running; the race's opening pan does this) the vehicle's own is drawn as
the game poses it. The copy keeps being published, so the vehicle's own wheel turns again as soon as
draws take it again, and the log notes both switches.
**The placeholder wheel.** `placeholder_steering_wheel = true` (off by default; F10 and the Quest
launcher's Settings > VR) draws a separate VR wheel for karts, or handlebar for bikes, whenever the
vehicle's own is not the one turning: with `native_steering_wheel = false`, and during the stretches
above where no draw takes the copy. Off, only the vehicle's own is seen, and the hands reach for it wherever its geometry is known,
turning or not; where it is not (a kart whose grips were not found), they reach for where the
placeholder would stand, in front of the seat.
Validated on the extracted PAL disc's 216 single-player kart/character and Mii combinations
(all 18 kart types): each resolves its node-0 bone as the runtime does and selects the complete
@@ -182,6 +182,13 @@ class SettingsPage(
write = { c, index -> c.setString("vr", "first_person_seat", SEATS[index]) },
enabledIf = firstPerson,
)
// runtime_config.h's kVrPlaceholderSteeringWheelDefault is off.
toggle(
R.string.vr_placeholder_wheel, R.string.vr_placeholder_wheel_helper,
read = { it.bool("vr", "placeholder_steering_wheel") ?: false },
write = { c, value -> c.setBool("vr", "placeholder_steering_wheel", value) },
enabledIf = cockpit,
)
// heurazy's grab-and-turn wheel: runtime_config.h's kVrHandSteeringDefault is on.
toggle(
R.string.vr_hand_steering, R.string.vr_hand_steering_helper,
@@ -533,6 +533,8 @@
<string name="vr_seat_helper">The cockpit puts you at the driver\'s eyes, life-size, with the steering wheel or handlebar turning within reach. Custom uses the head offsets from the in-headset settings.</string>
<string name="vr_seat_cockpit">Cockpit</string>
<string name="vr_seat_custom">Custom</string>
<string name="vr_placeholder_wheel">Placeholder wheel or handlebar</string>
<string name="vr_placeholder_wheel_helper">In the cockpit, draws a separate VR steering wheel (karts) or handlebar (bikes) whenever the vehicle\'s own is not the one turning, as in the race\'s opening pan. Off, you only see the vehicle\'s own.</string>
<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_cockpit_item_hand">Item in cockpit hand</string>
+23 -6
View File
@@ -76,6 +76,7 @@ struct RuntimeUserConfig {
std::optional<float> vrCockpitUnitsPerMeter;
std::optional<bool> vrSteeringWheel;
std::optional<bool> vrNativeSteeringWheel;
std::optional<bool> vrPlaceholderSteeringWheel;
std::optional<bool> vrObjectCulling;
std::optional<bool> vrHandSteering;
std::optional<std::string> vrCockpitItemHand;
@@ -231,13 +232,17 @@ inline constexpr float kVrCockpitUnitsPerMeterDefault = 100.0f;
inline constexpr float kVrCockpitUnitsPerMeterMin = 20.0f;
inline constexpr float kVrCockpitUnitsPerMeterMax = 400.0f;
// The vehicle's steering wheel or handlebar turns with the steering; the
// vehicle's own model is animated unless native_steering_wheel is off, which
// draws a separate VR wheel instead. Hand steering (grabbing that wheel with
// the tracked controllers, by heurazy) comes with it: the stick still steers
// until a grip actually takes hold of the wheel. Both launchers register
// hand_steering with this same default.
// vehicle's own model is animated unless native_steering_wheel is off.
// placeholder_steering_wheel draws a separate VR wheel or handlebar whenever
// the vehicle's own is not the one turning (native_steering_wheel off, or a
// draw that does not take the animated copy); off, the vehicle's own is all
// there is. Hand steering (grabbing that wheel with the tracked controllers,
// by heurazy) comes with it: the stick still steers until a grip actually
// takes hold of the wheel. Both launchers register hand_steering with this
// same default.
inline constexpr bool kVrSteeringWheelDefault = true;
inline constexpr bool kVrNativeSteeringWheelDefault = true;
inline constexpr bool kVrPlaceholderSteeringWheelDefault = false;
inline constexpr bool kVrHandSteeringDefault = true;
inline constexpr const char* kVrCockpitItemHandDefault = "left";
// A quick swing of the item hand forward or back throws the item that way.
@@ -594,9 +599,11 @@ inline void EnsureConfigFile() {
"first_person_rotation = \"yaw_pitch\"\n"
"# In the cockpit the vehicle's steering wheel or handlebar turns\n"
"# with the steering. native_steering_wheel animates the vehicle's\n"
"# own model; false draws a separate VR wheel instead.\n"
"# own model. placeholder_steering_wheel draws a separate VR wheel\n"
"# or handlebar whenever the vehicle's own is not the one turning.\n"
"steering_wheel = true\n"
"native_steering_wheel = true\n"
"placeholder_steering_wheel = false\n"
"# The game hides karts and objects its own camera cannot see.\n"
"# object_culling = false draws them anyway, so a head turn or a\n"
"# look over the shoulder shows them; it costs GPU time.\n"
@@ -887,6 +894,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
readRangedFloat("cockpit_units_per_meter", kVrCockpitUnitsPerMeterMin, kVrCockpitUnitsPerMeterMax);
config.vrSteeringWheel = FindConfigValue<bool>(document, "vr", "steering_wheel");
config.vrNativeSteeringWheel = FindConfigValue<bool>(document, "vr", "native_steering_wheel");
config.vrPlaceholderSteeringWheel = FindConfigValue<bool>(document, "vr", "placeholder_steering_wheel");
config.vrObjectCulling = FindConfigValue<bool>(document, "vr", "object_culling");
config.vrHandSteering = FindConfigValue<bool>(document, "vr", "hand_steering");
config.vrCockpitItemHand = FindConfigValue<std::string>(document, "vr", "cockpit_item_hand");
@@ -1283,6 +1291,11 @@ inline bool SetVrNativeSteeringWheel(bool value) {
return WriteSetting("vr", "native_steering_wheel", value ? "true" : "false");
}
inline bool SetVrPlaceholderSteeringWheel(bool value) {
Mutable().vrPlaceholderSteeringWheel = value;
return WriteSetting("vr", "placeholder_steering_wheel", value ? "true" : "false");
}
inline bool SetVrObjectCulling(bool value) {
Mutable().vrObjectCulling = value;
return WriteSetting("vr", "object_culling", value ? "true" : "false");
@@ -1787,6 +1800,10 @@ inline bool VrNativeSteeringWheel(bool fallback = kVrNativeSteeringWheelDefault)
return Get().vrNativeSteeringWheel.value_or(fallback);
}
inline bool VrPlaceholderSteeringWheel(bool fallback = kVrPlaceholderSteeringWheelDefault) {
return Get().vrPlaceholderSteeringWheel.value_or(fallback);
}
inline bool VrObjectCulling(bool fallback = kVrObjectCullingDefault) {
return Get().vrObjectCulling.value_or(fallback);
}
+9
View File
@@ -91,6 +91,15 @@ inline float MaxWheelAngle(bool bike, const WheelTuning& tuning) noexcept {
return clamped * 0.01745329252f;
}
// Whether the overlay draws a separate VR wheel or handlebar: only when
// placeholder_steering_wheel asks for one and the vehicle's own is not the one
// turning. Without it the hands reach for the vehicle's own wherever its
// geometry is known, turning or not.
inline bool DrawsPlaceholderControl(bool steering_wheel, bool native_steering_wheel, bool native_mesh_prepared,
bool placeholder) noexcept {
return placeholder && steering_wheel && !(native_steering_wheel && native_mesh_prepared);
}
// Grips only grab. As a Wii Remote they press nothing at all (C, the game's
// look-behind, is right B); as a gamepad they are the shoulders, so a holding
// hand's squeeze is released for the game. The wheel replaces the left stick's
+14 -4
View File
@@ -168,6 +168,7 @@ int g_vrFirstPersonSeat = RuntimeConfigFile::VrFirstPersonSeat() == "custom" ? 1
float g_vrCockpitUnitsPerMeter = RuntimeConfigFile::VrCockpitUnitsPerMeter();
bool g_vrSteeringWheel = RuntimeConfigFile::VrSteeringWheel();
bool g_vrNativeSteeringWheel = RuntimeConfigFile::VrNativeSteeringWheel();
bool g_vrPlaceholderSteeringWheel = RuntimeConfigFile::VrPlaceholderSteeringWheel();
bool g_vrObjectCulling = RuntimeConfigFile::VrObjectCulling();
bool g_vrHandSteering = RuntimeConfigFile::VrHandSteering();
constexpr std::array<const char*, 3> kVrCockpitItemHands{"Left", "Right", "Off"};
@@ -1214,9 +1215,15 @@ void DrawVrSteeringWheelSettings() {
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Turns the wheel or handlebar of the vehicle's own model. Off draws a "
"separate VR wheel instead, which is also what appears when a vehicle's "
"own wheel cannot be animated.");
ImGui::SetTooltip("Turns the wheel or handlebar of the vehicle's own model.");
}
if (ImGui::Checkbox("Placeholder wheel or handlebar", &g_vrPlaceholderSteeringWheel)) {
RuntimeConfigFile::SetVrPlaceholderSteeringWheel(g_vrPlaceholderSteeringWheel);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Draws a separate VR wheel (karts) or handlebar (bikes) whenever the "
"vehicle's own is not the one turning: with the option above off, or while "
"the game's draws do not take its animation, as in the race's opening pan.");
}
ImGui::EndDisabled();
if (ImGui::Checkbox("Hand steering (by heurazy)", &g_vrHandSteering)) {
@@ -1317,7 +1324,8 @@ void DrawVrSteeringWheelSettings() {
anchor.bike ? "handlebar" : "wheel",
!anchor.native_wheel.valid ? "grips not found"
: anchor.native_mesh_prepared ? "vehicle's own"
: "VR wheel",
: g_vrSteeringWheel && g_vrPlaceholderSteeringWheel ? "placeholder"
: "vehicle's own, not animated",
anchor.units_per_meter, GxNativeWheel::LastDrawCount());
}
}
@@ -1769,6 +1777,7 @@ void DrawVrCameraSettings() {
g_vrCockpitUnitsPerMeter = RuntimeConfigFile::kVrCockpitUnitsPerMeterDefault;
g_vrSteeringWheel = RuntimeConfigFile::kVrSteeringWheelDefault;
g_vrNativeSteeringWheel = RuntimeConfigFile::kVrNativeSteeringWheelDefault;
g_vrPlaceholderSteeringWheel = RuntimeConfigFile::kVrPlaceholderSteeringWheelDefault;
g_vrHandSteering = RuntimeConfigFile::kVrHandSteeringDefault;
g_vrCockpitItemHand = 0;
g_vrCockpitItemThrow = RuntimeConfigFile::kVrCockpitItemThrowDefault;
@@ -1776,6 +1785,7 @@ void DrawVrCameraSettings() {
RuntimeConfigFile::SetVrCockpitUnitsPerMeter(g_vrCockpitUnitsPerMeter);
RuntimeConfigFile::SetVrSteeringWheel(g_vrSteeringWheel);
RuntimeConfigFile::SetVrNativeSteeringWheel(g_vrNativeSteeringWheel);
RuntimeConfigFile::SetVrPlaceholderSteeringWheel(g_vrPlaceholderSteeringWheel);
RuntimeConfigFile::SetVrHandSteering(g_vrHandSteering);
RuntimeConfigFile::SetVrCockpitItemHand(RuntimeConfigFile::kVrCockpitItemHandDefault);
RuntimeConfigFile::SetVrCockpitItemThrow(g_vrCockpitItemThrow);
+9 -5
View File
@@ -1176,9 +1176,10 @@ bool ComputeCockpitAnchorLocked(const Mtx34& view_from_world, const KartPoseRead
return true;
}
// After the frame's draws: drop the wheel copies, and let the VR wheel stand
// in while no draw takes them (the race's opening pan, for one). Copies keep
// being published, so the vehicle's own wheel returns as soon as they match.
// After the frame's draws: drop the wheel copies, and let the placeholder VR
// wheel stand in (when enabled) while no draw takes them (the race's opening
// pan, for one). Copies keep being published, so the vehicle's own wheel
// returns as soon as they match.
void FinishNativeWheelFrameLocked() noexcept {
if (!g_state.wheel_arrays_posted) {
return;
@@ -1206,8 +1207,11 @@ void FinishNativeWheelFrameLocked() noexcept {
++g_state.native_wheel_switch_logs;
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] native steering wheel: no draw took the animated copy of vehicle 0x"
<< std::hex << g_state.cockpit.body << std::dec << " in "
<< kNativeWheelUnmatchedFrames
<< " frames; drawing the VR steering wheel until one does" << std::endl;
<< kNativeWheelUnmatchedFrames << " frames; "
<< (RuntimeConfigFile::VrPlaceholderSteeringWheel()
? "drawing the placeholder VR wheel until one does"
: "the vehicle's own is drawn as the game poses it until one does")
<< std::endl;
}
}
}
+5 -4
View File
@@ -1408,6 +1408,7 @@ void OpenXRInput::UpdateDriving(XrTime display_time, const driving::SeatFrame& s
const bool hand_steering = RuntimeConfigFile::VrHandSteering();
const bool steering_wheel = RuntimeConfigFile::VrSteeringWheel();
const bool native_steering_wheel = RuntimeConfigFile::VrNativeSteeringWheel();
const bool placeholder_steering_wheel = RuntimeConfigFile::VrPlaceholderSteeringWheel();
const float dt = m_wheel_time != 0 && display_time > m_wheel_time
? static_cast<float>(display_time - m_wheel_time) * 1.0e-9f
: 1.0f / 90.0f;
@@ -1417,10 +1418,10 @@ void OpenXRInput::UpdateDriving(XrTime display_time, const driving::SeatFrame& s
snapshot.cockpit_active = true;
snapshot.hand_steering = hand_steering;
snapshot.bike = anchor.bike;
// The vehicle's own control is the one turning (or none is shown at all),
// so the overlay adds no separate wheel.
snapshot.synthetic_control =
steering_wheel && !(native_steering_wheel && anchor.native_mesh_prepared);
// A separate wheel only when asked for and the vehicle's own is not the one
// turning.
snapshot.synthetic_control = driving::DrawsPlaceholderControl(
steering_wheel, native_steering_wheel, anchor.native_mesh_prepared, placeholder_steering_wheel);
// Which control the hands reach for: the vehicle's own wherever its
// geometry is known and no separate wheel is drawn, a handlebar always
+5
View File
@@ -26,6 +26,11 @@ int main() {
Require(Parse("[vr]\ncockpit_item_throw = false\n").vrCockpitItemThrow == false);
Require(Parse("[vr]\ncockpit_item_throw = true\n").vrCockpitItemThrow == true);
Require(!Parse("[vr]\n").vrCockpitItemThrow.has_value());
// [vr] placeholder_steering_wheel: the separate VR wheel or handlebar is opt-in.
Require(!RuntimeConfigFile::kVrPlaceholderSteeringWheelDefault);
Require(Parse("[vr]\nplaceholder_steering_wheel = true\n").vrPlaceholderSteeringWheel == true);
Require(Parse("[vr]\nplaceholder_steering_wheel = false\n").vrPlaceholderSteeringWheel == false);
Require(!Parse("[vr]\n").vrPlaceholderSteeringWheel.has_value());
// [vr] foveation: the Quest's foveated rendering level, index-matched to
// aurora_set_stereo_foveation.
for (std::string_view level : RuntimeConfigFile::kVrFoveationLevels) {
+16
View File
@@ -108,6 +108,21 @@ void TestSeatFrame() {
CheckNear(m[11], -1.0f, "lean turns up into forward", 1e-5f);
}
void TestPlaceholderControl() {
// Arguments: steering_wheel, native_steering_wheel, native_mesh_prepared, placeholder.
Check(!driving::DrawsPlaceholderControl(true, true, false, false),
"no placeholder while the vehicle's own is not animated, unless asked for");
Check(!driving::DrawsPlaceholderControl(true, false, false, false),
"no placeholder with the vehicle's own wheel off, unless asked for");
Check(driving::DrawsPlaceholderControl(true, true, false, true),
"the placeholder stands in while the vehicle's own is not animated");
Check(driving::DrawsPlaceholderControl(true, false, true, true),
"the placeholder replaces the vehicle's own wheel when that is off");
Check(!driving::DrawsPlaceholderControl(true, true, true, true),
"no placeholder while the vehicle's own is the one turning");
Check(!driving::DrawsPlaceholderControl(false, false, false, true), "no wheel turns at all");
}
} // namespace
int main() {
@@ -115,6 +130,7 @@ int main() {
TestMaxAngle();
TestVisual();
TestSeatFrame();
TestPlaceholderControl();
if (g_failures != 0) {
std::cerr << g_failures << " check(s) failed\n";
return 1;