mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 08:00:25 +02:00
Added First Person Camera Option
This commit is contained in:
1 parent
7898a76a22
commit
02e5cb60a4
18 files changed
+1219
-23
No files matched your search
@@ -55,6 +55,11 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> vrHudVirtualScreen;
|
||||
std::optional<bool> vrStopAtDisplayCopy;
|
||||
std::optional<bool> vrSkipCopyClears;
|
||||
std::optional<bool> vrFirstPerson;
|
||||
std::optional<float> vrFirstPersonUnitsPerMeter;
|
||||
std::optional<float> vrFirstPersonHeadUpMeters;
|
||||
std::optional<float> vrFirstPersonHeadForwardMeters;
|
||||
std::optional<float> vrFirstPersonHeadRightMeters;
|
||||
std::optional<float> audioVolume;
|
||||
std::optional<float> audioMusicVolume;
|
||||
std::optional<float> audioSoundEffectsVolume;
|
||||
@@ -339,7 +344,18 @@ inline void EnsureConfigFile() {
|
||||
"# reset a GX copy performs afterwards. Both keep that reset\n"
|
||||
"# from erasing the eye, and both are safe to turn off.\n"
|
||||
"stop_at_display_copy = true\n"
|
||||
"skip_copy_clears = true\n\n"
|
||||
"skip_copy_clears = true\n"
|
||||
"# Put the camera at the Player 1 driver's head instead of behind\n"
|
||||
"# the kart, with the horizon kept level. Changeable live from the\n"
|
||||
"# F10 menu, and only during a single-screen race. The world scale\n"
|
||||
"# 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"
|
||||
"# Where the head sits in the kart's own frame, in metres.\n"
|
||||
"first_person_head_up_meters = 1.0\n"
|
||||
"first_person_head_forward_meters = 12.0\n"
|
||||
"first_person_head_right_meters = 0.0\n\n"
|
||||
"[audio]\n"
|
||||
"volume = 1.0\n"
|
||||
"music_volume = 1.0\n"
|
||||
@@ -500,6 +516,23 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.vrHudVirtualScreen = FindConfigValue<bool>(document, "vr", "hud_virtual_screen");
|
||||
config.vrStopAtDisplayCopy = FindConfigValue<bool>(document, "vr", "stop_at_display_copy");
|
||||
config.vrSkipCopyClears = FindConfigValue<bool>(document, "vr", "skip_copy_clears");
|
||||
config.vrFirstPerson = FindConfigValue<bool>(document, "vr", "first_person");
|
||||
if (auto value = FindConfigFloat(document, "vr", "first_person_units_per_meter");
|
||||
value && *value >= 1.0f && *value <= 10000.0f) {
|
||||
config.vrFirstPersonUnitsPerMeter = *value;
|
||||
}
|
||||
if (auto value = FindConfigFloat(document, "vr", "first_person_head_up_meters");
|
||||
value && *value >= -3.0f && *value <= 3.0f) {
|
||||
config.vrFirstPersonHeadUpMeters = *value;
|
||||
}
|
||||
if (auto value = FindConfigFloat(document, "vr", "first_person_head_forward_meters");
|
||||
value && *value >= -3.0f && *value <= 3.0f) {
|
||||
config.vrFirstPersonHeadForwardMeters = *value;
|
||||
}
|
||||
if (auto value = FindConfigFloat(document, "vr", "first_person_head_right_meters");
|
||||
value && *value >= -3.0f && *value <= 3.0f) {
|
||||
config.vrFirstPersonHeadRightMeters = *value;
|
||||
}
|
||||
|
||||
auto readVolume = [&](std::string_view key) -> std::optional<float> {
|
||||
auto value = FindConfigFloat(document, "audio", key);
|
||||
@@ -742,6 +775,43 @@ inline bool SetVrSkipCopyClears(bool value) {
|
||||
return WriteSetting("vr", "skip_copy_clears", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPerson(bool value) {
|
||||
Mutable().vrFirstPerson = value;
|
||||
return WriteSetting("vr", "first_person", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonUnitsPerMeter(float value) {
|
||||
value = std::clamp(value, 1.0f, 10000.0f);
|
||||
Mutable().vrFirstPersonUnitsPerMeter = value;
|
||||
std::ostringstream formatted;
|
||||
formatted << value;
|
||||
return WriteSetting("vr", "first_person_units_per_meter", formatted.str());
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonHeadUpMeters(float value) {
|
||||
value = std::clamp(value, -3.0f, 3.0f);
|
||||
Mutable().vrFirstPersonHeadUpMeters = value;
|
||||
std::ostringstream formatted;
|
||||
formatted << value;
|
||||
return WriteSetting("vr", "first_person_head_up_meters", formatted.str());
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonHeadForwardMeters(float value) {
|
||||
value = std::clamp(value, -3.0f, 3.0f);
|
||||
Mutable().vrFirstPersonHeadForwardMeters = value;
|
||||
std::ostringstream formatted;
|
||||
formatted << value;
|
||||
return WriteSetting("vr", "first_person_head_forward_meters", formatted.str());
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonHeadRightMeters(float value) {
|
||||
value = std::clamp(value, -3.0f, 3.0f);
|
||||
Mutable().vrFirstPersonHeadRightMeters = value;
|
||||
std::ostringstream formatted;
|
||||
formatted << value;
|
||||
return WriteSetting("vr", "first_person_head_right_meters", formatted.str());
|
||||
}
|
||||
|
||||
inline bool SetControllerButton(size_t index, std::string value) {
|
||||
if (index >= kControllerButtonKeys.size()) {
|
||||
return false;
|
||||
@@ -990,6 +1060,26 @@ inline bool VrSkipCopyClears(bool fallback = true) {
|
||||
return Get().vrSkipCopyClears.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool VrFirstPerson(bool fallback = false) {
|
||||
return Get().vrFirstPerson.value_or(fallback);
|
||||
}
|
||||
|
||||
inline float VrFirstPersonUnitsPerMeter(float fallback = 10.0f) {
|
||||
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 VrFirstPersonHeadForwardMeters(float fallback = 0.0f) {
|
||||
return std::clamp(Get().vrFirstPersonHeadForwardMeters.value_or(fallback), -20.0f, 20.0f);
|
||||
}
|
||||
|
||||
inline float VrFirstPersonHeadRightMeters(float fallback = 0.0f) {
|
||||
return std::clamp(Get().vrFirstPersonHeadRightMeters.value_or(fallback), -3.0f, 3.0f);
|
||||
}
|
||||
|
||||
inline std::string GraphicsApi(std::string fallback = "auto") {
|
||||
return Get().graphicsApi.value_or(std::move(fallback));
|
||||
}
|
||||
|
||||
@@ -12,4 +12,8 @@ void Draw() noexcept;
|
||||
bool StartupScreenVisible() noexcept;
|
||||
void NotifyStrapInputAccepted() noexcept;
|
||||
void AdvancePresentedFrame() noexcept;
|
||||
// Re-sends the VR virtual screen's placement to Aurora. Its metres are
|
||||
// converted with the world scale currently in effect, so switching the
|
||||
// first-person camera on or off has to repeat it.
|
||||
void RefreshVrHudVirtualScreen() noexcept;
|
||||
} // namespace settings_overlay
|
||||
@@ -0,0 +1,205 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
// A row-major affine 3x4, the same shape and convention as an NW4R/GX Mtx and
|
||||
// as Aurora's Mat3x4: a point is transformed as out = M * (p, 1).
|
||||
using Mtx34 = std::array<float, 12>;
|
||||
|
||||
inline constexpr Mtx34 kIdentityMtx34{
|
||||
1.0f, 0.0f, 0.0f, 0.0f, //
|
||||
0.0f, 1.0f, 0.0f, 0.0f, //
|
||||
0.0f, 0.0f, 1.0f, 0.0f,
|
||||
};
|
||||
|
||||
// Where the driver's head sits in the kart's own frame, in metres. The kart
|
||||
// frame is the EGG convention: +x right, +y up, +z forward.
|
||||
struct FirstPersonHeadOffsets {
|
||||
float right = 0.0f;
|
||||
float up = 1.0f;
|
||||
float forward = 0.0f;
|
||||
};
|
||||
|
||||
// The camera relocation published to Aurora for one guest frame: a transform
|
||||
// from the game's recorded view space into the space the headset renders from.
|
||||
struct FirstPersonAnchor {
|
||||
Mtx34 anchor_from_scene = kIdentityMtx34;
|
||||
bool valid = false;
|
||||
uint64_t guest_frame_index = 0;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure math. Header-only and free of guest access, so it is directly testable.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline constexpr float kAnchorEpsilon = 1.0e-6f;
|
||||
|
||||
inline bool IsFiniteFloat(const float* value) noexcept {
|
||||
// The runtime is built with -ffast-math, which permits the compiler to fold
|
||||
// std::isfinite to true. Inspect the object representation instead, the way
|
||||
// the presentation policy validates its own floats.
|
||||
uint32_t bits = 0;
|
||||
std::memcpy(&bits, value, sizeof(bits));
|
||||
return (bits & 0x7F800000u) != 0x7F800000u;
|
||||
}
|
||||
|
||||
inline bool IsFiniteMtx34(const Mtx34& value) noexcept {
|
||||
for (const float& element : value) {
|
||||
if (!IsFiniteFloat(&element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct Vec3 {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
};
|
||||
|
||||
inline float Dot(const Vec3& a, const Vec3& b) noexcept {
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z;
|
||||
}
|
||||
|
||||
inline Vec3 Cross(const Vec3& a, const Vec3& b) noexcept {
|
||||
return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
|
||||
}
|
||||
|
||||
inline bool Normalize(Vec3& value) noexcept {
|
||||
const float length_squared = Dot(value, value);
|
||||
if (!IsFiniteFloat(&length_squared) || !(length_squared > kAnchorEpsilon)) {
|
||||
return false;
|
||||
}
|
||||
const float inverse_length = 1.0f / std::sqrt(length_squared);
|
||||
value.x *= inverse_length;
|
||||
value.y *= inverse_length;
|
||||
value.z *= inverse_length;
|
||||
return true;
|
||||
}
|
||||
|
||||
// out = matrix * (x, y, z, 1)
|
||||
inline Vec3 TransformPoint(const Mtx34& matrix, float x, float y, float z) noexcept {
|
||||
return {
|
||||
matrix[0] * x + matrix[1] * y + matrix[2] * z + matrix[3],
|
||||
matrix[4] * x + matrix[5] * y + matrix[6] * z + matrix[7],
|
||||
matrix[8] * x + matrix[9] * y + matrix[10] * z + matrix[11],
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Builds the anchor from the game's view matrix (world -> recorded view space),
|
||||
// the kart's pose (kart-local -> world), and head offsets already converted to
|
||||
// world units.
|
||||
//
|
||||
// The translation moves the camera onto the head. With level_horizon the
|
||||
// rotation keeps the recorded camera's heading but drops its pitch and roll, so
|
||||
// the headset owns pitch and roll outright; without it the recorded camera's
|
||||
// orientation is kept whole and only the eye moves. Returns false and leaves
|
||||
// `out` untouched when the inputs cannot produce an orthonormal frame.
|
||||
inline bool ComputeFirstPersonAnchor(const Mtx34& view_from_world, const Mtx34& kart_from_local,
|
||||
float head_right_units, float head_up_units,
|
||||
float head_forward_units, bool level_horizon,
|
||||
Mtx34& out) noexcept {
|
||||
using namespace detail;
|
||||
if (!IsFiniteMtx34(view_from_world) || !IsFiniteMtx34(kart_from_local)) {
|
||||
return false;
|
||||
}
|
||||
const Vec3 head_world =
|
||||
TransformPoint(kart_from_local, head_right_units, head_up_units, head_forward_units);
|
||||
const Vec3 a = TransformPoint(view_from_world, head_world.x, head_world.y, head_world.z);
|
||||
if (!IsFiniteFloat(&a.x) || !IsFiniteFloat(&a.y) || !IsFiniteFloat(&a.z)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rows of the anchor's rotation. Identity keeps the recorded camera's own
|
||||
// orientation and moves the eye only.
|
||||
Vec3 rows[3]{{1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}};
|
||||
if (level_horizon) {
|
||||
// World +Y in view coordinates: the column of the view rotation that
|
||||
// the world up axis selects.
|
||||
Vec3 up{view_from_world[1], view_from_world[5], view_from_world[9]};
|
||||
if (!Normalize(up)) {
|
||||
return false;
|
||||
}
|
||||
// Level the recorded camera's forward (-Z in its own space) onto the
|
||||
// horizon plane. Looking near-straight up or down leaves nothing to
|
||||
// project, so recover the heading from the camera's up axis instead.
|
||||
const Vec3 camera_forward{0.0f, 0.0f, -1.0f};
|
||||
float along = Dot(camera_forward, up);
|
||||
Vec3 forward{camera_forward.x - up.x * along, camera_forward.y - up.y * along,
|
||||
camera_forward.z - up.z * along};
|
||||
if (!Normalize(forward)) {
|
||||
const Vec3 camera_up{0.0f, 1.0f, 0.0f};
|
||||
along = Dot(camera_up, up);
|
||||
forward = {camera_up.x - up.x * along, camera_up.y - up.y * along,
|
||||
camera_up.z - up.z * along};
|
||||
if (!Normalize(forward)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Vec3 right = Cross(forward, up);
|
||||
if (!Normalize(right)) {
|
||||
return false;
|
||||
}
|
||||
// Re-derive up from the orthonormalized pair so a slightly non-rigid
|
||||
// view matrix cannot leave a skewed frame behind.
|
||||
rows[0] = right;
|
||||
rows[1] = Cross(right, forward);
|
||||
rows[2] = {-forward.x, -forward.y, -forward.z};
|
||||
}
|
||||
|
||||
Mtx34 anchor{};
|
||||
for (uint32_t row = 0; row < 3; ++row) {
|
||||
anchor[row * 4 + 0] = rows[row].x;
|
||||
anchor[row * 4 + 1] = rows[row].y;
|
||||
anchor[row * 4 + 2] = rows[row].z;
|
||||
anchor[row * 4 + 3] = -Dot(rows[row], a);
|
||||
}
|
||||
if (!IsFiniteMtx34(anchor)) {
|
||||
return false;
|
||||
}
|
||||
out = anchor;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-frame observation. Called from the translated-code observers on the guest
|
||||
// thread; the anchor is consumed by the producer at its Aurora frame seal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Enables anchor computation and sets the head offsets and world scale used to
|
||||
// convert them. Called whenever the configuration or the F10 toggle changes.
|
||||
void MkwVRFirstPersonConfigure(bool enabled, const FirstPersonHeadOffsets& offsets,
|
||||
float units_per_meter) noexcept;
|
||||
|
||||
// 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.
|
||||
void MkwVRFirstPersonApplyConfiguredSettings() noexcept;
|
||||
|
||||
// Reads the race camera and the player's kart and republishes the anchor. Call
|
||||
// once per guest frame, after the kart and camera updates and before the draws.
|
||||
// race_camera_address is the frame's own RaceCamera, or zero if none was seen.
|
||||
void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_address) noexcept;
|
||||
|
||||
// Drops every captured pointer and the held anchor. Call on race entry/exit.
|
||||
void MkwVRFirstPersonReset() noexcept;
|
||||
|
||||
// Producer-side read. Thread-safe. A valid anchor is also what marks the mode
|
||||
// as engaged, and so what selects the first-person world scale: it is invalid
|
||||
// whenever the mode is off, the race has not produced a usable anchor, or the
|
||||
// anchor has been missing long enough to give up holding the last one.
|
||||
FirstPersonAnchor MkwVRFirstPersonGetAnchor() noexcept;
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -78,6 +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;
|
||||
};
|
||||
|
||||
struct MkwVRSceneObservation {
|
||||
@@ -108,6 +113,10 @@ struct MkwVRPolicySnapshot {
|
||||
MkwVRCameraObservation camera{};
|
||||
uint32_t available_bindings = MkwVRBindingNone;
|
||||
bool session_active = false;
|
||||
// A first-person camera is actually driving the view this frame. Set by the
|
||||
// integration layer once the anchor it publishes to the renderer is valid,
|
||||
// so the world scale can never disagree with where the camera is.
|
||||
bool first_person_engaged = false;
|
||||
// Changes whenever the stable presentation-safety state changes. Ordinary
|
||||
// per-frame scene/camera publication does not advance it.
|
||||
uint64_t safety_generation = 1;
|
||||
@@ -115,6 +124,13 @@ struct MkwVRPolicySnapshot {
|
||||
// the current presentation mode, so a transient scene/camera mismatch
|
||||
// cannot accept an immersive packet from an adjacent asynchronous frame.
|
||||
uint64_t content_tag = 0;
|
||||
|
||||
// The scale headset translation and IPD are converted at. Head offsets in
|
||||
// metres must use the same value, or the camera and the world disagree.
|
||||
float EffectiveUnitsPerMeter() const noexcept {
|
||||
return first_person_engaged ? config.first_person_units_per_meter
|
||||
: config.world_units_per_meter;
|
||||
}
|
||||
};
|
||||
|
||||
// All policy functions are thread-safe. Publishing functions are intended for
|
||||
@@ -127,6 +143,14 @@ void MkwVRPolicySetAvailableBindings(uint32_t bindings) noexcept;
|
||||
void MkwVRPolicyPublishScene(const MkwVRSceneObservation& scene) noexcept;
|
||||
void MkwVRPolicyPublishRaceCamera(const MkwVRCameraObservation& camera) noexcept;
|
||||
void MkwVRPolicyInvalidateRaceCamera() noexcept;
|
||||
// Reported by the integration layer each time the first-person anchor engages
|
||||
// or disengages. It selects the world scale and nothing else: presentation
|
||||
// safety is unaffected, so this never advances the safety generation.
|
||||
void MkwVRPolicySetFirstPersonEngaged(bool engaged) noexcept;
|
||||
// Live world scale for the first-person camera. Separate from
|
||||
// MkwVRPolicyConfigure so the F10 slider can retune it during a race without
|
||||
// republishing (and revalidating) the whole configuration.
|
||||
void MkwVRPolicySetFirstPersonUnitsPerMeter(float units_per_meter) noexcept;
|
||||
MkwVRPolicySnapshot MkwVRPolicyGetSnapshot() noexcept;
|
||||
|
||||
// This classifier is deliberately structural rather than heuristic: future
|
||||
|
||||
Reference in new issue
Block a user