mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 08:00:25 +02:00
Added First-person "Hide Driver" and "Hide Driver and Kart" & Improved Default FPV Settings
This commit is contained in:
1 parent
2742b8d9a2
commit
6440c5bc10
7 files changed
+394
-46
No files matched your search
@@ -30,10 +30,12 @@ hud_virtual_screen = true
|
||||
stop_at_display_copy = true
|
||||
skip_copy_clears = true
|
||||
first_person = false
|
||||
first_person_units_per_meter = 10.0
|
||||
first_person_head_up_meters = 1.0
|
||||
first_person_units_per_meter = 30.0
|
||||
first_person_head_up_meters = 3.0
|
||||
first_person_head_forward_meters = 0.0
|
||||
first_person_head_right_meters = 0.0
|
||||
first_person_hide_driver = true
|
||||
first_person_hidden_model = 0
|
||||
```
|
||||
|
||||
Set `enabled = true`, close the game completely, and start it again. These settings are read only
|
||||
@@ -61,8 +63,8 @@ four are live and are also exposed in the F10 settings bar.
|
||||
By default the headset sits where Mario Kart's own chase camera sits, and `world_units_per_meter`
|
||||
of 500 presents the race as a small diorama on a table. Turning on `first_person` moves the camera
|
||||
to the Player 1 driver's head instead, and switches the world scale to
|
||||
`first_person_units_per_meter`, which defaults to the 10 units per metre Mario Kart Wii is
|
||||
authored at, so the race reads life-size.
|
||||
`first_person_units_per_meter`, whose default of 30 is what makes the race read life-size from the
|
||||
seat. It is a matter of taste rather than a property of the game, so the F10 bar exposes it.
|
||||
|
||||
The game's own transforms are never modified. Each guest frame the runtime reads the race camera's
|
||||
view matrix and the player kart's physics pose and derives one affine transform from the recorded
|
||||
@@ -82,11 +84,23 @@ immersive stereo. Menus, split-screen, and the virtual-screen fallback are unaff
|
||||
the desktop mirror, which keeps showing the game's ordinary third-person view. If the kart or
|
||||
camera cannot be read the camera stays where the game put it rather than guessing.
|
||||
|
||||
Two limitations are worth knowing. Mario Kart still culls the scene from its own chase camera, so
|
||||
a wide head turn in first person can reveal the edge of what the game decided to draw. And the
|
||||
driver's own head is still rendered; nudge `first_person_head_forward_meters` if it intrudes. As
|
||||
with the rest of the race instrumentation, the object offsets this reads are specific to the
|
||||
project's supported PAL `RMCP01` translation.
|
||||
Your own driver sits exactly where your eyes are, so their head would fill the view.
|
||||
`first_person_hide_driver` removes it. The game applies one draw byte across every model of a kart
|
||||
and to its body, so clearing it outright takes the vehicle along with the driver;
|
||||
`first_person_hidden_model` names a single model to hide instead. On PAL `RMCP01` a kart carries two
|
||||
models and index `0` is the driver, which is the default: the character goes and the vehicle stays.
|
||||
`-1` restores the blunt behaviour and hides everything. An index the kart does not have hides
|
||||
nothing, and the log reports how many it has when the mode engages. The F10 bar presents this as
|
||||
two toggles, "Hide driver" and "Hide driver and kart", alongside a button that restores every
|
||||
first-person default. Both settings touch your own kart
|
||||
only, so the other racers are untouched, and the original values are restored when first person
|
||||
stops or the race ends. This is the one place the first-person camera modifies the game rather than
|
||||
only reading it.
|
||||
|
||||
One limitation is worth knowing: Mario Kart still culls the scene from its own chase camera, so a
|
||||
wide head turn in first person can reveal the edge of what the game decided to draw. As with the
|
||||
rest of the race instrumentation, the object offsets this reads are specific to the project's
|
||||
supported PAL `RMCP01` translation.
|
||||
|
||||
## Presentation policy
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ struct RuntimeUserConfig {
|
||||
std::optional<float> vrFirstPersonHeadUpMeters;
|
||||
std::optional<float> vrFirstPersonHeadForwardMeters;
|
||||
std::optional<float> vrFirstPersonHeadRightMeters;
|
||||
std::optional<bool> vrFirstPersonHideDriver;
|
||||
std::optional<int32_t> vrFirstPersonHiddenModel;
|
||||
std::optional<float> audioVolume;
|
||||
std::optional<float> audioMusicVolume;
|
||||
std::optional<float> audioSoundEffectsVolume;
|
||||
@@ -134,6 +136,17 @@ inline constexpr const char* kPortableUserDataDirectoryName = "UserData";
|
||||
// installation.
|
||||
inline constexpr int kPortableSearchDepth = 4;
|
||||
|
||||
// First-person camera defaults and the range its head offsets accept, in one
|
||||
// place: the config getters, the on-disk template and the F10 bar's reset all
|
||||
// read them from here, so they cannot drift apart again.
|
||||
inline constexpr float kVrFirstPersonUnitsPerMeterDefault = 30.0f;
|
||||
inline constexpr float kVrFirstPersonHeadUpDefault = 3.0f;
|
||||
inline constexpr float kVrFirstPersonHeadForwardDefault = 0.0f;
|
||||
inline constexpr float kVrFirstPersonHeadRightDefault = 0.0f;
|
||||
inline constexpr bool kVrFirstPersonHideDriverDefault = true;
|
||||
inline constexpr int32_t kVrFirstPersonHiddenModelDefault = 0;
|
||||
inline constexpr float kVrFirstPersonHeadOffsetLimit = 10.0f;
|
||||
|
||||
inline std::string Trim(std::string_view text) {
|
||||
size_t begin = 0;
|
||||
while (begin < text.size() && std::isspace(static_cast<unsigned char>(text[begin]))) {
|
||||
@@ -351,11 +364,18 @@ inline void EnsureConfigFile() {
|
||||
"# below replaces world_units_per_meter while it is engaged: 10 is\n"
|
||||
"# life-size, where the 500 above makes the race a small diorama.\n"
|
||||
"first_person = false\n"
|
||||
"first_person_units_per_meter = 10.0\n"
|
||||
"first_person_units_per_meter = 30.0\n"
|
||||
"# Where the head sits in the kart's own frame, in metres.\n"
|
||||
"first_person_head_up_meters = 1.0\n"
|
||||
"first_person_head_up_meters = 3.0\n"
|
||||
"first_person_head_forward_meters = 12.0\n"
|
||||
"first_person_head_right_meters = 0.0\n\n"
|
||||
"first_person_head_right_meters = 0.0\n"
|
||||
"# In first person the driver sits where your eyes are. Hiding\n"
|
||||
"# the driver removes the head that would otherwise be in the\n"
|
||||
"# way; hiding the kart removes the vehicle around you too.\n"
|
||||
"first_person_hide_driver = true\n"
|
||||
"# 0 is the driver, which is the usual choice. -1 hides every\n"
|
||||
"# model of your kart, the vehicle included.\n"
|
||||
"first_person_hidden_model = 0\n\n"
|
||||
"[audio]\n"
|
||||
"volume = 1.0\n"
|
||||
"music_volume = 1.0\n"
|
||||
@@ -522,17 +542,26 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.vrFirstPersonUnitsPerMeter = *value;
|
||||
}
|
||||
if (auto value = FindConfigFloat(document, "vr", "first_person_head_up_meters");
|
||||
value && *value >= -3.0f && *value <= 3.0f) {
|
||||
value && *value >= -kVrFirstPersonHeadOffsetLimit &&
|
||||
*value <= kVrFirstPersonHeadOffsetLimit) {
|
||||
config.vrFirstPersonHeadUpMeters = *value;
|
||||
}
|
||||
if (auto value = FindConfigFloat(document, "vr", "first_person_head_forward_meters");
|
||||
value && *value >= -3.0f && *value <= 3.0f) {
|
||||
value && *value >= -kVrFirstPersonHeadOffsetLimit &&
|
||||
*value <= kVrFirstPersonHeadOffsetLimit) {
|
||||
config.vrFirstPersonHeadForwardMeters = *value;
|
||||
}
|
||||
if (auto value = FindConfigFloat(document, "vr", "first_person_head_right_meters");
|
||||
value && *value >= -3.0f && *value <= 3.0f) {
|
||||
value && *value >= -kVrFirstPersonHeadOffsetLimit &&
|
||||
*value <= kVrFirstPersonHeadOffsetLimit) {
|
||||
config.vrFirstPersonHeadRightMeters = *value;
|
||||
}
|
||||
config.vrFirstPersonHideDriver =
|
||||
FindConfigValue<bool>(document, "vr", "first_person_hide_driver");
|
||||
if (auto value = FindConfigInt(document, "vr", "first_person_hidden_model");
|
||||
value && *value >= -1 && *value <= 31) {
|
||||
config.vrFirstPersonHiddenModel = static_cast<int32_t>(*value);
|
||||
}
|
||||
|
||||
auto readVolume = [&](std::string_view key) -> std::optional<float> {
|
||||
auto value = FindConfigFloat(document, "audio", key);
|
||||
@@ -789,7 +818,7 @@ inline bool SetVrFirstPersonUnitsPerMeter(float value) {
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonHeadUpMeters(float value) {
|
||||
value = std::clamp(value, -3.0f, 3.0f);
|
||||
value = std::clamp(value, -kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
|
||||
Mutable().vrFirstPersonHeadUpMeters = value;
|
||||
std::ostringstream formatted;
|
||||
formatted << value;
|
||||
@@ -797,15 +826,28 @@ inline bool SetVrFirstPersonHeadUpMeters(float value) {
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonHeadForwardMeters(float value) {
|
||||
value = std::clamp(value, -3.0f, 3.0f);
|
||||
value = std::clamp(value, -kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
|
||||
Mutable().vrFirstPersonHeadForwardMeters = value;
|
||||
std::ostringstream formatted;
|
||||
formatted << value;
|
||||
return WriteSetting("vr", "first_person_head_forward_meters", formatted.str());
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonHideDriver(bool value) {
|
||||
Mutable().vrFirstPersonHideDriver = value;
|
||||
return WriteSetting("vr", "first_person_hide_driver", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonHiddenModel(int32_t value) {
|
||||
value = std::clamp(value, -1, 31);
|
||||
Mutable().vrFirstPersonHiddenModel = value;
|
||||
std::ostringstream formatted;
|
||||
formatted << value;
|
||||
return WriteSetting("vr", "first_person_hidden_model", formatted.str());
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonHeadRightMeters(float value) {
|
||||
value = std::clamp(value, -3.0f, 3.0f);
|
||||
value = std::clamp(value, -kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
|
||||
Mutable().vrFirstPersonHeadRightMeters = value;
|
||||
std::ostringstream formatted;
|
||||
formatted << value;
|
||||
@@ -1064,20 +1106,31 @@ inline bool VrFirstPerson(bool fallback = false) {
|
||||
return Get().vrFirstPerson.value_or(fallback);
|
||||
}
|
||||
|
||||
inline float VrFirstPersonUnitsPerMeter(float fallback = 10.0f) {
|
||||
inline float VrFirstPersonUnitsPerMeter(float fallback = kVrFirstPersonUnitsPerMeterDefault) {
|
||||
return std::clamp(Get().vrFirstPersonUnitsPerMeter.value_or(fallback), 1.0f, 10000.0f);
|
||||
}
|
||||
|
||||
inline float VrFirstPersonHeadUpMeters(float fallback = 1.0f) {
|
||||
return std::clamp(Get().vrFirstPersonHeadUpMeters.value_or(fallback), -3.0f, 3.0f);
|
||||
inline float VrFirstPersonHeadUpMeters(float fallback = kVrFirstPersonHeadUpDefault) {
|
||||
return std::clamp(Get().vrFirstPersonHeadUpMeters.value_or(fallback),
|
||||
-kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
|
||||
}
|
||||
|
||||
inline float VrFirstPersonHeadForwardMeters(float fallback = 0.0f) {
|
||||
return std::clamp(Get().vrFirstPersonHeadForwardMeters.value_or(fallback), -20.0f, 20.0f);
|
||||
inline float VrFirstPersonHeadForwardMeters(float fallback = kVrFirstPersonHeadForwardDefault) {
|
||||
return std::clamp(Get().vrFirstPersonHeadForwardMeters.value_or(fallback),
|
||||
-kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
|
||||
}
|
||||
|
||||
inline float VrFirstPersonHeadRightMeters(float fallback = 0.0f) {
|
||||
return std::clamp(Get().vrFirstPersonHeadRightMeters.value_or(fallback), -3.0f, 3.0f);
|
||||
inline float VrFirstPersonHeadRightMeters(float fallback = kVrFirstPersonHeadRightDefault) {
|
||||
return std::clamp(Get().vrFirstPersonHeadRightMeters.value_or(fallback),
|
||||
-kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
|
||||
}
|
||||
|
||||
inline bool VrFirstPersonHideDriver(bool fallback = kVrFirstPersonHideDriverDefault) {
|
||||
return Get().vrFirstPersonHideDriver.value_or(fallback);
|
||||
}
|
||||
|
||||
inline int32_t VrFirstPersonHiddenModel(int32_t fallback = kVrFirstPersonHiddenModelDefault) {
|
||||
return std::clamp(Get().vrFirstPersonHiddenModel.value_or(fallback), -1, 31);
|
||||
}
|
||||
|
||||
inline std::string GraphicsApi(std::string fallback = "auto") {
|
||||
|
||||
@@ -23,7 +23,7 @@ inline constexpr Mtx34 kIdentityMtx34{
|
||||
// frame is the EGG convention: +x right, +y up, +z forward.
|
||||
struct FirstPersonHeadOffsets {
|
||||
float right = 0.0f;
|
||||
float up = 1.0f;
|
||||
float up = 3.0f;
|
||||
float forward = 0.0f;
|
||||
};
|
||||
|
||||
@@ -183,6 +183,10 @@ inline bool ComputeFirstPersonAnchor(const Mtx34& view_from_world, const Mtx34&
|
||||
void MkwVRFirstPersonConfigure(bool enabled, const FirstPersonHeadOffsets& offsets,
|
||||
float units_per_meter) noexcept;
|
||||
|
||||
// While the anchor is driving the view the player's own models can be removed,
|
||||
// since the driver otherwise sits exactly where the eyes are. This uses the
|
||||
// game's own visibility fields, and puts them back when it stops.
|
||||
//
|
||||
// Reads the current [vr] first-person settings and applies them here and to the
|
||||
// presentation policy's world scale. The single place those settings are
|
||||
// interpreted, shared by startup and the F10 settings bar.
|
||||
|
||||
@@ -78,11 +78,11 @@ struct MkwVRPolicyConfig {
|
||||
// immersive race HUD so 2D content keeps its place across the transition.
|
||||
float hud_width_meters = 2.4f;
|
||||
float hud_scale = 1.0f;
|
||||
// World scale used while the first-person camera is engaged. Mario Kart
|
||||
// Wii is authored at roughly this many units per metre, so it is what
|
||||
// makes the race read life-size; the third-person default deliberately
|
||||
// does not, presenting the race as a small diorama instead.
|
||||
float first_person_units_per_meter = 10.0f;
|
||||
// World scale used while the first-person camera is engaged. This is what
|
||||
// makes the race read life-size from the driver's seat; the third-person
|
||||
// default deliberately does not, presenting it as a small diorama instead.
|
||||
// Kept in step with RuntimeConfigFile's default, which the F10 bar resets to.
|
||||
float first_person_units_per_meter = 30.0f;
|
||||
};
|
||||
|
||||
struct MkwVRSceneObservation {
|
||||
|
||||
@@ -106,10 +106,12 @@ bool g_vrStopAtDisplayCopy = RuntimeConfigFile::VrStopAtDisplayCopy(true);
|
||||
bool g_vrSkipCopyClears = RuntimeConfigFile::VrSkipCopyClears(true);
|
||||
bool g_vrHudVirtualScreen = RuntimeConfigFile::VrHudVirtualScreen(true);
|
||||
bool g_vrFirstPerson = RuntimeConfigFile::VrFirstPerson(false);
|
||||
float g_vrFirstPersonUnitsPerMeter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter(10.0f);
|
||||
float g_vrFirstPersonHeadUp = RuntimeConfigFile::VrFirstPersonHeadUpMeters(1.0f);
|
||||
float g_vrFirstPersonHeadForward = RuntimeConfigFile::VrFirstPersonHeadForwardMeters(0.0f);
|
||||
float g_vrFirstPersonHeadRight = RuntimeConfigFile::VrFirstPersonHeadRightMeters(0.0f);
|
||||
float g_vrFirstPersonUnitsPerMeter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter();
|
||||
float g_vrFirstPersonHeadUp = RuntimeConfigFile::VrFirstPersonHeadUpMeters();
|
||||
float g_vrFirstPersonHeadForward = RuntimeConfigFile::VrFirstPersonHeadForwardMeters();
|
||||
float g_vrFirstPersonHeadRight = RuntimeConfigFile::VrFirstPersonHeadRightMeters();
|
||||
bool g_vrFirstPersonHideDriver = RuntimeConfigFile::VrFirstPersonHideDriver();
|
||||
int g_vrFirstPersonHiddenModel = RuntimeConfigFile::VrFirstPersonHiddenModel();
|
||||
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0);
|
||||
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
|
||||
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
|
||||
@@ -889,11 +891,11 @@ void DrawGraphicsSettings() {
|
||||
}
|
||||
bool headOffsetsChanged = false;
|
||||
headOffsetsChanged |=
|
||||
ImGui::SliderFloat("Head height (m)", &g_vrFirstPersonHeadUp, -1.0f, 3.0f, "%.2f");
|
||||
ImGui::SliderFloat("Head height (m)", &g_vrFirstPersonHeadUp, -RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, "%.2f");
|
||||
headOffsetsChanged |=
|
||||
ImGui::SliderFloat("Head forward (m)", &g_vrFirstPersonHeadForward, -20.0f, 20.0f, "%.2f");
|
||||
headOffsetsChanged |=
|
||||
ImGui::SliderFloat("Head sideways (m)", &g_vrFirstPersonHeadRight, -3.0f, 3.0f, "%.2f");
|
||||
ImGui::SliderFloat("Head sideways (m)", &g_vrFirstPersonHeadRight, -RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, "%.2f");
|
||||
if (headOffsetsChanged) {
|
||||
RuntimeConfigFile::SetVrFirstPersonHeadUpMeters(g_vrFirstPersonHeadUp);
|
||||
RuntimeConfigFile::SetVrFirstPersonHeadForwardMeters(g_vrFirstPersonHeadForward);
|
||||
@@ -901,10 +903,52 @@ void DrawGraphicsSettings() {
|
||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||
}
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::TextDisabled(
|
||||
"Where the head sits in the kart's own frame. Nudge it forward if the driver's own "
|
||||
"head intrudes on the view.");
|
||||
ImGui::TextDisabled("Where the head sits in the kart's own frame.");
|
||||
ImGui::PopTextWrapPos();
|
||||
// Two presentations of one setting: which models go, or none at all.
|
||||
// Ticking either replaces the other, and unticking both shows everything.
|
||||
const auto applyHiding = [](bool enabled, int model) {
|
||||
g_vrFirstPersonHideDriver = enabled;
|
||||
if (enabled) {
|
||||
g_vrFirstPersonHiddenModel = model;
|
||||
RuntimeConfigFile::SetVrFirstPersonHiddenModel(model);
|
||||
}
|
||||
RuntimeConfigFile::SetVrFirstPersonHideDriver(enabled);
|
||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||
};
|
||||
bool hideDriver = g_vrFirstPersonHideDriver && g_vrFirstPersonHiddenModel >= 0;
|
||||
bool hideDriverAndKart = g_vrFirstPersonHideDriver && g_vrFirstPersonHiddenModel < 0;
|
||||
if (ImGui::Checkbox("Hide driver", &hideDriver)) {
|
||||
applyHiding(hideDriver, RuntimeConfigFile::kVrFirstPersonHiddenModelDefault);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Removes your character and leaves the kart around you. Their head would "
|
||||
"otherwise be where your eyes are. Other racers are unaffected.");
|
||||
}
|
||||
if (ImGui::Checkbox("Hide driver and kart", &hideDriverAndKart)) {
|
||||
applyHiding(hideDriverAndKart, -1);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Removes the vehicle as well, leaving nothing of your own kart.");
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Reset first-person defaults")) {
|
||||
g_vrFirstPersonUnitsPerMeter = RuntimeConfigFile::kVrFirstPersonUnitsPerMeterDefault;
|
||||
g_vrFirstPersonHeadUp = RuntimeConfigFile::kVrFirstPersonHeadUpDefault;
|
||||
g_vrFirstPersonHeadForward = RuntimeConfigFile::kVrFirstPersonHeadForwardDefault;
|
||||
g_vrFirstPersonHeadRight = RuntimeConfigFile::kVrFirstPersonHeadRightDefault;
|
||||
g_vrFirstPersonHideDriver = RuntimeConfigFile::kVrFirstPersonHideDriverDefault;
|
||||
g_vrFirstPersonHiddenModel = RuntimeConfigFile::kVrFirstPersonHiddenModelDefault;
|
||||
RuntimeConfigFile::SetVrFirstPersonUnitsPerMeter(g_vrFirstPersonUnitsPerMeter);
|
||||
RuntimeConfigFile::SetVrFirstPersonHeadUpMeters(g_vrFirstPersonHeadUp);
|
||||
RuntimeConfigFile::SetVrFirstPersonHeadForwardMeters(g_vrFirstPersonHeadForward);
|
||||
RuntimeConfigFile::SetVrFirstPersonHeadRightMeters(g_vrFirstPersonHeadRight);
|
||||
RuntimeConfigFile::SetVrFirstPersonHideDriver(g_vrFirstPersonHideDriver);
|
||||
RuntimeConfigFile::SetVrFirstPersonHiddenModel(g_vrFirstPersonHiddenModel);
|
||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||
ApplyVrHudVirtualScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawFpsOverlay() {
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <mutex>
|
||||
|
||||
extern "C" void func_805A6C58(CpuContext* context);
|
||||
extern "C" void func_8056A470(CpuContext* context);
|
||||
extern "C" void func_8056A580(CpuContext* context);
|
||||
|
||||
namespace mkw::vr {
|
||||
namespace {
|
||||
@@ -68,6 +70,31 @@ constexpr uint32_t kKartBodyPhysicsOffset = 0x90u;
|
||||
// visual pose, and is the alternative to try if the seat ever looks detached.
|
||||
constexpr uint32_t kKartPhysicsPoseOffset = 0x9Cu;
|
||||
|
||||
// Kart::Link::GetModelsVisibility (0x8059108C) is proxy -> accessor -> +0x58.
|
||||
// Kart::ModelsVisibility::SetInvisible (0x8056A2F0) is nothing but two stores,
|
||||
// a u16 at +0x10 and a u8 at +0x12.
|
||||
//
|
||||
// Writing them is not enough on its own. UpdateModelsVisibility (0x8056A470)
|
||||
// is what carries the byte at +0x12 out to the models, looping over them and
|
||||
// calling a virtual through the secondary vtable at +0x0C, and the game runs it
|
||||
// during the kart update -- before the draw boundary where this can write. So
|
||||
// the write has to be followed by running that function again, or nothing ever
|
||||
// reads it. Measured on device, the mask at +0x10 is already 0 in normal play,
|
||||
// so it is not the field that decides what draws.
|
||||
constexpr uint32_t kKartAccessorModelsVisibilityOffset = 0x58u;
|
||||
constexpr uint32_t kModelsVisibilityMaskOffset = 0x10u;
|
||||
constexpr uint32_t kModelsVisibilityDrawOffset = 0x12u;
|
||||
|
||||
// That one byte reaches every model the loop visits, which is why clearing it
|
||||
// takes the kart along with the driver. The loop walks an array of models: from
|
||||
// the holder at *(visibility[0] + 0x14), entries start at +0xD8 with a stride
|
||||
// of 4 and the count sits at +0xF0. SetModelDraw (0x8056A580) applies the byte
|
||||
// to a single one of them, so naming an index hides exactly that model.
|
||||
constexpr uint32_t kModelsVisibilityHolderOffset = 0x14u;
|
||||
constexpr uint32_t kModelHolderArrayOffset = 0xD8u;
|
||||
constexpr uint32_t kModelHolderCountOffset = 0xF0u;
|
||||
constexpr uint32_t kMaxPlayerModels = 32;
|
||||
|
||||
// Offline Mario Kart Wii puts the local racer first, and immersive
|
||||
// presentation already requires exactly one on-screen player.
|
||||
constexpr uint32_t kLocalPlayerIndex = 0;
|
||||
@@ -188,10 +215,27 @@ KartPoseRead ReadPlayerKartPose(Mtx34& out) noexcept {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Hiding the player's own models. Like the anchor this only reads the game to
|
||||
// decide what to write, but unlike the anchor it does modify it, so it owns the
|
||||
// values it displaced and puts them back when it stops.
|
||||
struct ModelVisibilityState {
|
||||
bool hide_driver = false;
|
||||
// -1 hides every model of the player's kart, the vehicle included; a valid
|
||||
// index hides only that model. Index 0 is the driver on PAL RMCP01.
|
||||
int hidden_model = 0;
|
||||
bool saved = false;
|
||||
uint32_t saved_object = 0;
|
||||
uint16_t original_mask = 0;
|
||||
uint8_t original_draw = 0;
|
||||
bool logged = false;
|
||||
bool logged_models = false;
|
||||
bool logged_range = false;
|
||||
};
|
||||
|
||||
struct FirstPersonState {
|
||||
bool enabled = false;
|
||||
FirstPersonHeadOffsets offsets{};
|
||||
float units_per_meter = 10.0f;
|
||||
float units_per_meter = RuntimeConfigFile::kVrFirstPersonUnitsPerMeterDefault;
|
||||
|
||||
uint32_t camera_address = 0;
|
||||
// Armed by the draw boundary, consumed by the frame seal.
|
||||
@@ -211,6 +255,176 @@ struct FirstPersonState {
|
||||
|
||||
std::mutex g_mutex;
|
||||
FirstPersonState g_state;
|
||||
ModelVisibilityState g_visibility;
|
||||
|
||||
// Walks to the player's ModelsVisibility, or zero when the race is not up.
|
||||
uint32_t ResolveModelsVisibility() noexcept {
|
||||
uint32_t manager = 0;
|
||||
uint32_t players = 0;
|
||||
uint32_t proxy = 0;
|
||||
uint32_t accessor = 0;
|
||||
uint32_t visibility = 0;
|
||||
if (!ReadGuestPointer(kKartManagerInstanceAddress, manager) ||
|
||||
!ReadGuestPointer(manager + kKartManagerPlayersOffset, players) ||
|
||||
!ReadGuestPointer(players + kLocalPlayerIndex * 4u, proxy) ||
|
||||
!ReadGuestPointer(proxy + kKartProxyAccessorOffset, accessor) ||
|
||||
!ReadGuestPointer(accessor + kKartAccessorModelsVisibilityOffset, visibility)) {
|
||||
return 0;
|
||||
}
|
||||
return visibility;
|
||||
}
|
||||
|
||||
// Carries the visibility fields out to the models, the way the kart update
|
||||
// does. Without this the fields are just bytes nothing has read.
|
||||
void ApplyModelsVisibilityToModels(uint32_t visibility) noexcept {
|
||||
const CpuContext* context = TryGetCpuContext();
|
||||
if (context == nullptr || visibility == 0) {
|
||||
return;
|
||||
}
|
||||
CpuContext call_context = *context;
|
||||
call_context.gpr[3] = visibility;
|
||||
try {
|
||||
CpuContextScope scope(&call_context);
|
||||
func_8056A470(&call_context);
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
}
|
||||
}
|
||||
|
||||
// Applies the draw byte to one model only. Bounded and pointer-checked because
|
||||
// this ends in a virtual call on a guest object.
|
||||
bool ApplyModelDraw(uint32_t visibility, uint32_t holder, uint32_t index) noexcept {
|
||||
uint32_t model = 0;
|
||||
if (!ReadGuestPointer(holder + kModelHolderArrayOffset + index * 4u, model)) {
|
||||
return false;
|
||||
}
|
||||
const CpuContext* context = TryGetCpuContext();
|
||||
if (context == nullptr || !Memory::Contains(model, 4)) {
|
||||
return false;
|
||||
}
|
||||
CpuContext call_context = *context;
|
||||
call_context.gpr[3] = visibility;
|
||||
call_context.gpr[4] = model;
|
||||
try {
|
||||
CpuContextScope scope(&call_context);
|
||||
func_8056A580(&call_context);
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The model array the visibility loop walks, or zero when it cannot be reached.
|
||||
uint32_t ResolveModelHolder(uint32_t visibility, uint32_t& count) noexcept {
|
||||
uint32_t holder = 0;
|
||||
uint32_t owner = 0;
|
||||
count = 0;
|
||||
if (!ReadGuestPointer(visibility, owner) ||
|
||||
!ReadGuestPointer(owner + kModelsVisibilityHolderOffset, holder) ||
|
||||
!Memory::Contains(holder + kModelHolderCountOffset, 4)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
count = Memory::Read32(holder + kModelHolderCountOffset);
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
return 0;
|
||||
}
|
||||
if (count == 0 || count > kMaxPlayerModels) {
|
||||
count = 0;
|
||||
return 0;
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
void RestoreModelVisibilityLocked() noexcept {
|
||||
if (!g_visibility.saved) {
|
||||
return;
|
||||
}
|
||||
if (Memory::Contains(g_visibility.saved_object + kModelsVisibilityDrawOffset, 1)) {
|
||||
try {
|
||||
Memory::Write16(g_visibility.saved_object + kModelsVisibilityMaskOffset,
|
||||
g_visibility.original_mask);
|
||||
Memory::Write8(g_visibility.saved_object + kModelsVisibilityDrawOffset,
|
||||
g_visibility.original_draw);
|
||||
ApplyModelsVisibilityToModels(g_visibility.saved_object);
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
}
|
||||
}
|
||||
g_visibility.saved = false;
|
||||
g_visibility.saved_object = 0;
|
||||
}
|
||||
|
||||
// Applied at the draw boundary: the kart update has set these for the frame and
|
||||
// nothing has drawn yet.
|
||||
void ApplyModelVisibilityLocked() noexcept {
|
||||
if (!g_visibility.hide_driver) {
|
||||
RestoreModelVisibilityLocked();
|
||||
return;
|
||||
}
|
||||
const uint32_t visibility = ResolveModelsVisibility();
|
||||
if (visibility == 0 ||
|
||||
!Memory::Contains(visibility + kModelsVisibilityDrawOffset, 1)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!g_visibility.saved || g_visibility.saved_object != visibility) {
|
||||
RestoreModelVisibilityLocked();
|
||||
g_visibility.original_mask =
|
||||
Memory::Read16(visibility + kModelsVisibilityMaskOffset);
|
||||
g_visibility.original_draw =
|
||||
Memory::Read8(visibility + kModelsVisibilityDrawOffset);
|
||||
g_visibility.saved_object = visibility;
|
||||
g_visibility.saved = true;
|
||||
if (!g_visibility.logged) {
|
||||
g_visibility.logged = true;
|
||||
// The values the game normally holds. If clearing the byte on
|
||||
// its own does not remove the driver, these say which bits of
|
||||
// the mask are worth trying instead.
|
||||
RT_LOG(RT_TAG_RUNTIME)
|
||||
<< "[mkw-vr] model visibility: object=0x" << std::hex << visibility
|
||||
<< ", mask=0x" << g_visibility.original_mask << ", driver=0x"
|
||||
<< static_cast<uint32_t>(g_visibility.original_draw) << std::dec
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
uint32_t count = 0;
|
||||
const uint32_t holder = ResolveModelHolder(visibility, count);
|
||||
if (!g_visibility.logged_models && holder != 0) {
|
||||
g_visibility.logged_models = true;
|
||||
RT_LOG(RT_TAG_RUNTIME)
|
||||
<< "[mkw-vr] model visibility: " << count
|
||||
<< " models; set first_person_hidden_model to one of 0.." << (count - 1)
|
||||
<< " to hide a single one, or -1 for all of them" << std::endl;
|
||||
}
|
||||
const bool index_in_range =
|
||||
g_visibility.hidden_model >= 0 && holder != 0 &&
|
||||
static_cast<uint32_t>(g_visibility.hidden_model) < count;
|
||||
if (g_visibility.hidden_model >= 0 && !index_in_range) {
|
||||
// Naming a model the kart does not have should leave it alone, not
|
||||
// silently fall through to hiding all of them.
|
||||
if (!g_visibility.logged_range) {
|
||||
g_visibility.logged_range = true;
|
||||
RT_LOG(RT_TAG_RUNTIME)
|
||||
<< "[mkw-vr] model visibility: model " << g_visibility.hidden_model
|
||||
<< " is out of range for this kart's " << count
|
||||
<< "; nothing hidden" << std::endl;
|
||||
}
|
||||
} else if (index_in_range) {
|
||||
// Show everything, then take back the one model that is named.
|
||||
Memory::Write8(visibility + kModelsVisibilityDrawOffset,
|
||||
g_visibility.original_draw);
|
||||
ApplyModelsVisibilityToModels(visibility);
|
||||
Memory::Write8(visibility + kModelsVisibilityDrawOffset, 0);
|
||||
ApplyModelDraw(visibility, holder,
|
||||
static_cast<uint32_t>(g_visibility.hidden_model));
|
||||
Memory::Write8(visibility + kModelsVisibilityDrawOffset,
|
||||
g_visibility.original_draw);
|
||||
} else {
|
||||
Memory::Write8(visibility + kModelsVisibilityDrawOffset, 0);
|
||||
ApplyModelsVisibilityToModels(visibility);
|
||||
}
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
}
|
||||
}
|
||||
|
||||
void LogAnchorLocked(uint64_t frame, const Mtx34& anchor, const Mtx34& view_from_world,
|
||||
const KartPoseRead& kart, const Mtx34& kart_from_local) noexcept {
|
||||
@@ -294,18 +508,28 @@ void MkwVRFirstPersonConfigure(bool enabled, const FirstPersonHeadOffsets& offse
|
||||
}
|
||||
|
||||
void MkwVRFirstPersonApplyConfiguredSettings() noexcept {
|
||||
const float units_per_meter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter(10.0f);
|
||||
const float units_per_meter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter();
|
||||
const FirstPersonHeadOffsets offsets{
|
||||
RuntimeConfigFile::VrFirstPersonHeadRightMeters(0.0f),
|
||||
RuntimeConfigFile::VrFirstPersonHeadUpMeters(1.0f),
|
||||
RuntimeConfigFile::VrFirstPersonHeadForwardMeters(0.0f),
|
||||
RuntimeConfigFile::VrFirstPersonHeadRightMeters(),
|
||||
RuntimeConfigFile::VrFirstPersonHeadUpMeters(),
|
||||
RuntimeConfigFile::VrFirstPersonHeadForwardMeters(),
|
||||
};
|
||||
MkwVRFirstPersonConfigure(RuntimeConfigFile::VrFirstPerson(false), offsets, units_per_meter);
|
||||
MkwVRPolicySetFirstPersonUnitsPerMeter(units_per_meter);
|
||||
{
|
||||
// Same lock the guest thread applies these under.
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_visibility.hide_driver = RuntimeConfigFile::VrFirstPersonHideDriver();
|
||||
g_visibility.hidden_model = RuntimeConfigFile::VrFirstPersonHiddenModel();
|
||||
}
|
||||
}
|
||||
|
||||
void MkwVRFirstPersonReset() noexcept {
|
||||
std::lock_guard lock(g_mutex);
|
||||
RestoreModelVisibilityLocked();
|
||||
g_visibility.logged = false;
|
||||
g_visibility.logged_models = false;
|
||||
g_visibility.logged_range = false;
|
||||
g_state.armed = false;
|
||||
g_state.armed_view_valid = false;
|
||||
g_state.camera_address = 0;
|
||||
@@ -323,11 +547,20 @@ void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_add
|
||||
g_state.anchor = {};
|
||||
g_state.hold_frames = 0;
|
||||
g_state.armed = false;
|
||||
RestoreModelVisibilityLocked();
|
||||
return;
|
||||
}
|
||||
g_state.armed = true;
|
||||
g_state.armed_frame = guest_frame_index;
|
||||
g_state.armed_view_valid = ReadSceneViewMatrix(g_state.armed_view);
|
||||
// Uses last frame's verdict, since this frame's anchor is not computed
|
||||
// until the seal. One frame of lag on hiding a model is not visible, and
|
||||
// it keeps the player's kart drawn whenever the anchor is not engaged.
|
||||
if (g_state.anchor.valid) {
|
||||
ApplyModelVisibilityLocked();
|
||||
} else {
|
||||
RestoreModelVisibilityLocked();
|
||||
}
|
||||
}
|
||||
|
||||
void MkwVRFirstPersonCommit() noexcept {
|
||||
|
||||
@@ -45,7 +45,7 @@ void ConfigurePolicy(bool enabled) noexcept {
|
||||
config.world_units_per_meter = RuntimeConfigFile::VrWorldUnitsPerMeter(500.0f);
|
||||
config.hud_distance_meters = RuntimeConfigFile::VrHudDistanceMeters(2.0f);
|
||||
config.hud_width_meters = RuntimeConfigFile::VrHudWidthMeters(2.4f);
|
||||
config.first_person_units_per_meter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter(10.0f);
|
||||
config.first_person_units_per_meter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter();
|
||||
MkwVRPolicyConfigure(config);
|
||||
MkwVRInstrumentationInitialize();
|
||||
MkwVRFirstPersonApplyConfiguredSettings();
|
||||
|
||||
Reference in new issue
Block a user