Added "Lean back angle" option

This commit is contained in:
iChris4 committed 2026-09-08 20:58:49 +02:00
1 parent 6e7e0f075b
commit 913eda2b3a
4 files changed
+99 -5

No files matched your search

+28 -1
View File
@@ -63,6 +63,7 @@ struct RuntimeUserConfig {
std::optional<bool> vrFirstPersonHideDriver;
std::optional<int32_t> vrFirstPersonHiddenModel;
std::optional<std::string> vrRecenterKey;
std::optional<float> vrLeanBackDegrees;
std::optional<float> audioVolume;
std::optional<float> audioMusicVolume;
std::optional<float> audioSoundEffectsVolume;
@@ -150,6 +151,10 @@ inline constexpr float kVrFirstPersonHeadOffsetLimit = 10.0f;
// SDL scancode name, spelled the way SDL_GetScancodeName produces it. An
// empty string leaves the recenter hotkey unbound, menu button only.
inline constexpr std::string_view kVrRecenterKeyDefault = "F9";
// Fixed pitch of the game camera for a player sitting reclined, in degrees.
// Positive leans the camera back with you; 0 disables it entirely.
inline constexpr float kVrLeanBackDegreesDefault = 0.0f;
inline constexpr float kVrLeanBackDegreesLimit = 45.0f;
inline std::string Trim(std::string_view text) {
size_t begin = 0;
@@ -387,7 +392,12 @@ inline void EnsureConfigFile() {
"# the game. Applies during an immersive race; the menu screen is\n"
"# head-locked already. Rebindable from the F10 menu under VR. Leave\n"
"# empty to unbind.\n"
"recenter_key = \"F9\"\n\n"
"recenter_key = \"F9\"\n"
"# Fixed pitch of the game camera, in degrees, for a player sitting\n"
"# reclined. Positive tilts the camera back with you, so 20 here cancels\n"
"# reclining about 20 degrees and puts the track back in front of you.\n"
"# 0 disables it. Changeable live from the F10 menu under VR.\n"
"lean_back_degrees = 0.0\n\n"
"[audio]\n"
"volume = 1.0\n"
"music_volume = 1.0\n"
@@ -573,6 +583,10 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
if (auto value = FindConfigValue<std::string>(document, "vr", "recenter_key")) {
config.vrRecenterKey = *value;
}
if (auto value = FindConfigFloat(document, "vr", "lean_back_degrees");
value && *value >= -kVrLeanBackDegreesLimit && *value <= kVrLeanBackDegreesLimit) {
config.vrLeanBackDegrees = static_cast<float>(*value);
}
if (auto value = FindConfigInt(document, "vr", "first_person_hidden_model");
value && *value >= -1 && *value <= 31) {
config.vrFirstPersonHiddenModel = static_cast<int32_t>(*value);
@@ -853,6 +867,14 @@ inline bool SetVrRecenterKey(std::string value) {
return WriteSetting("vr", "recenter_key", FormatString(value));
}
inline bool SetVrLeanBackDegrees(float value) {
value = std::clamp(value, -kVrLeanBackDegreesLimit, kVrLeanBackDegreesLimit);
Mutable().vrLeanBackDegrees = value;
std::ostringstream formatted;
formatted << value;
return WriteSetting("vr", "lean_back_degrees", formatted.str());
}
inline bool SetVrFirstPersonHideDriver(bool value) {
Mutable().vrFirstPersonHideDriver = value;
return WriteSetting("vr", "first_person_hide_driver", value ? "true" : "false");
@@ -1149,6 +1171,11 @@ inline std::string VrRecenterKey(std::string fallback = std::string(kVrRecenterK
return Get().vrRecenterKey.value_or(std::move(fallback));
}
inline float VrLeanBackDegrees(float fallback = kVrLeanBackDegreesDefault) {
return std::clamp(Get().vrLeanBackDegrees.value_or(fallback),
-kVrLeanBackDegreesLimit, kVrLeanBackDegreesLimit);
}
inline bool VrFirstPersonHideDriver(bool fallback = kVrFirstPersonHideDriverDefault) {
return Get().vrFirstPersonHideDriver.value_or(fallback);
}
+8
View File
@@ -44,4 +44,12 @@ std::string OpenXRLastError();
// immersive race, where the head-locked menu screen has no origin to recenter.
void OpenXRRequestRecenter() noexcept;
// Sets a fixed pitch of the game camera for a player sitting reclined, in
// degrees, clamped to RuntimeConfigFile::kVrLeanBackDegreesLimit. Positive
// tilts the camera back with the player, so an angle matching how far they are
// reclined puts the track back in front of them. 0 applies no pitch at all.
// Applies to the immersive race view only; callable from any thread and read
// once per published frame.
void OpenXRSetLeanBackDegrees(float degrees) noexcept;
} // namespace mkw::vr
+15
View File
@@ -121,6 +121,7 @@ SDL_Scancode g_vrRecenterScancode = [] {
: SDL_GetScancodeFromName(name.c_str());
}();
bool g_vrRecenterRebinding = false;
float g_vrLeanBackDegrees = RuntimeConfigFile::VrLeanBackDegrees();
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0);
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
@@ -909,6 +910,20 @@ void DrawVrSettings() {
ImGui::SetTooltip("Click to rebind. Esc cancels, Backspace unbinds.");
}
}
if (ImGui::SliderFloat("Lean back angle (deg)", &g_vrLeanBackDegrees,
-RuntimeConfigFile::kVrLeanBackDegreesLimit,
RuntimeConfigFile::kVrLeanBackDegreesLimit, "%.1f")) {
mkw::vr::OpenXRSetLeanBackDegrees(g_vrLeanBackDegrees);
RuntimeConfigFile::SetVrLeanBackDegrees(g_vrLeanBackDegrees);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Tilts the game camera back with you when you play reclined, so set it to "
"roughly how far back your seat is and the track comes back in front of you "
"instead of above. 0 applies no tilt. Unlike recentering, this deliberately "
"does pitch the view, and looking sideways while it is set will roll the "
"horizon the way a real recline would.");
}
ImGui::Separator();
ImGui::Text("VR camera");
if (ImGui::Checkbox("First-person camera", &g_vrFirstPerson)) {
+48 -4
View File
@@ -37,6 +37,8 @@
namespace mkw::vr {
namespace {
inline constexpr float kDegreesToRadians = 0.01745329252f;
void ConfigurePolicy(bool enabled) noexcept {
MkwVRPolicyReset();
MkwVRPolicyConfig config{};
@@ -78,6 +80,15 @@ Quaternion Conjugate(Quaternion value) noexcept {
return {-value.x, -value.y, -value.z, value.w};
}
Quaternion Multiply(const Quaternion& left, const Quaternion& right) noexcept {
return Normalize({
left.w * right.x + left.x * right.w + left.y * right.z - left.z * right.y,
left.w * right.y - left.x * right.z + left.y * right.w + left.z * right.x,
left.w * right.z + left.x * right.y - left.y * right.x + left.z * right.w,
left.w * right.w - left.x * right.x - left.y * right.y - left.z * right.z,
});
}
std::array<float, 3> Rotate(const Quaternion& q, const std::array<float, 3>& value) noexcept {
// Expanded q * [v,0] * conjugate(q), avoiding two temporary normalizations.
const float tx = 2.0f * (q.y * value[2] - q.z * value[1]);
@@ -155,13 +166,26 @@ void ProjectionFromFov(const XrFovf& fov, float output[16]) noexcept {
// leaves the game's horizon level and its forward fixed to the reference space.
// Composing a latched head orientation in here instead would bake that instant's
// pitch and roll into the neutral and tilt the horizon for the rest of the session.
//
// lean_back_radians is the one deliberate exception: a fixed pitch of the game
// camera about the reference space's right axis, for a player sitting reclined.
// It multiplies in on the right, so it turns the world before the head rotation
// rather than after it, which is what makes it cancel a reclined head exactly
// and, when you then look sideways, roll the view the way a real recline would.
void ViewFromBase(const XrPosef& eye_pose, const std::array<float, 3>& base_position,
bool position_valid, float units_per_meter, float output[12]) noexcept {
bool position_valid, float units_per_meter, float lean_back_radians,
float output[12]) noexcept {
const Quaternion eye = Normalize({eye_pose.orientation.x, eye_pose.orientation.y,
eye_pose.orientation.z, eye_pose.orientation.w});
const Quaternion inverse_eye = Conjugate(eye);
float rotation[9];
RotationMatrix(inverse_eye, rotation);
if (lean_back_radians == 0.0f) {
RotationMatrix(inverse_eye, rotation);
} else {
const float half_angle = 0.5f * lean_back_radians;
const Quaternion lean{std::sin(half_angle), 0.0f, 0.0f, std::cos(half_angle)};
RotationMatrix(Multiply(inverse_eye, lean), rotation);
}
std::array<float, 3> translation{};
if (position_valid) {
@@ -326,6 +350,13 @@ public:
recenter_requested_.store(true, std::memory_order_release);
}
void SetLeanBackDegrees(float degrees) noexcept {
lean_back_degrees_.store(
std::clamp(degrees, -RuntimeConfigFile::kVrLeanBackDegreesLimit,
RuntimeConfigFile::kVrLeanBackDegreesLimit),
std::memory_order_relaxed);
}
void ServiceProducerFrameBoundary() noexcept {
if (teardown_requested_.load(std::memory_order_acquire)) {
Shutdown();
@@ -596,12 +627,16 @@ private:
base_position_valid_ = true;
}
last_immersive_ = true;
// Read once so both eyes are built from the same angle even if the
// settings slider moves between them.
const float lean_back_radians =
lean_back_degrees_.load(std::memory_order_relaxed) * kDegreesToRadians;
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
ProjectionFromFov(source.xr_frame.views[eye].fov,
destination.eyes[eye].projection);
ViewFromBase(source.xr_frame.views[eye].pose, base_position_,
position_valid && base_position_valid_,
units_per_meter, destination.eyes[eye].viewFromCenter);
position_valid && base_position_valid_, units_per_meter,
lean_back_radians, destination.eyes[eye].viewFromCenter);
}
}
@@ -644,6 +679,7 @@ private:
std::atomic_bool running_{false};
std::atomic_bool teardown_requested_{false};
std::atomic_bool recenter_requested_{false};
std::atomic<float> lean_back_degrees_{RuntimeConfigFile::VrLeanBackDegrees()};
std::atomic<PublishedFrame*> published_{nullptr};
PublishedFrame published_frame_{};
std::mutex published_mutex_;
@@ -722,6 +758,14 @@ void OpenXRRequestRecenter() noexcept {
#endif
}
void OpenXRSetLeanBackDegrees(float degrees) noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
OpenXRIntegration::Get().SetLeanBackDegrees(degrees);
#else
(void)degrees;
#endif
}
std::string OpenXRLastError() {
#if !defined(MKW_ENABLE_OPENXR)
return "this build was compiled without OpenXR support";