mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
Added Yaw only / Yaw + Pitch / Full rotation options to first-person
This commit is contained in:
1 parent
bd106dd1fe
commit
0e3e2c3dc0
6 files changed
+283
-42
No files matched your search
@@ -36,6 +36,7 @@ first_person_head_forward_meters = 0.0
|
||||
first_person_head_right_meters = 0.0
|
||||
first_person_hide_driver = true
|
||||
first_person_hidden_model = 0
|
||||
first_person_rotation = "yaw"
|
||||
```
|
||||
|
||||
Set `enabled = true`, close the game completely, and start it again. These settings are read only
|
||||
@@ -73,11 +74,17 @@ the renderer composes it onto every perspective draw's model-view matrix, alongs
|
||||
own per-eye delta. The kart's *physics* pose is used deliberately, not the animated model: an
|
||||
animated frame would bob and lurch the camera.
|
||||
|
||||
Only the camera's heading is taken from the game. Its pitch and roll are dropped, so the horizon
|
||||
stays level through a chase-camera tilt or a banked corner, and the headset owns pitch, roll, and
|
||||
free look outright. The head's place in the kart is `first_person_head_up_meters` and its two
|
||||
companions, measured in the kart's own frame; the F10 sliders exist because the comfortable value
|
||||
is a matter of taste and is best judged from inside the headset.
|
||||
`first_person_rotation` decides where the view's orientation comes from, mirroring DolphinXR's
|
||||
camera-anchor modes. `"yaw"`, the default, keeps the horizon level through a chase-camera tilt or a
|
||||
banked corner. `"yaw_pitch"` adds the kart's climb, so a slope or a wheelie tips the view while a
|
||||
banked corner still never rolls it. `"full"` takes the kart's whole orientation, banking included.
|
||||
All three are the same construction from a forward and an up axis, differing only in which pair
|
||||
they take: pairing a forward with world up is what removes roll. The headset always adds free look
|
||||
on top of whichever is chosen, and only the translation onto the head is common to all three.
|
||||
|
||||
The head's place in the kart is `first_person_head_up_meters` and its two companions, measured in
|
||||
the kart's own frame; the F10 sliders exist because the comfortable value is a matter of taste and
|
||||
is best judged from inside the headset.
|
||||
|
||||
The mode engages only in a single-screen race, the same content that already qualifies for
|
||||
immersive stereo. Menus, split-screen, and the virtual-screen fallback are unaffected, and so is
|
||||
|
||||
@@ -62,6 +62,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<float> vrFirstPersonHeadRightMeters;
|
||||
std::optional<bool> vrFirstPersonHideDriver;
|
||||
std::optional<int32_t> vrFirstPersonHiddenModel;
|
||||
std::optional<std::string> vrFirstPersonRotation;
|
||||
std::optional<std::string> vrRecenterKey;
|
||||
std::optional<float> vrLeanBackDegrees;
|
||||
std::optional<float> audioVolume;
|
||||
@@ -148,6 +149,12 @@ inline constexpr float kVrFirstPersonHeadRightDefault = 0.0f;
|
||||
inline constexpr bool kVrFirstPersonHideDriverDefault = true;
|
||||
inline constexpr int32_t kVrFirstPersonHiddenModelDefault = 0;
|
||||
inline constexpr float kVrFirstPersonHeadOffsetLimit = 10.0f;
|
||||
// "yaw", "yaw_pitch" or "full", matching FirstPersonRotation.
|
||||
inline constexpr const char* kVrFirstPersonRotationDefault = "yaw";
|
||||
|
||||
inline bool IsSupportedVrFirstPersonRotation(std::string_view value) {
|
||||
return value == "yaw" || value == "yaw_pitch" || value == "full";
|
||||
}
|
||||
// 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";
|
||||
@@ -385,6 +392,10 @@ inline void EnsureConfigFile() {
|
||||
"# 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"
|
||||
"# Where the view's orientation comes from: \"yaw\" levels the\n"
|
||||
"# horizon, \"yaw_pitch\" adds the kart's climb but no roll, and\n"
|
||||
"# \"full\" takes the kart's whole orientation so the view banks.\n"
|
||||
"first_person_rotation = \"yaw\"\n\n"
|
||||
"# Keyboard shortcut that recenters the VR view, naming the key the\n"
|
||||
"# way SDL does (F9, Home, Keypad 5, ...). It moves the race view to\n"
|
||||
"# where you are sitting now and brings the menu screen back upright in\n"
|
||||
@@ -587,6 +598,10 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
value && *value >= -kVrLeanBackDegreesLimit && *value <= kVrLeanBackDegreesLimit) {
|
||||
config.vrLeanBackDegrees = static_cast<float>(*value);
|
||||
}
|
||||
if (auto value = FindConfigValue<std::string>(document, "vr", "first_person_rotation");
|
||||
value && IsSupportedVrFirstPersonRotation(*value)) {
|
||||
config.vrFirstPersonRotation = *value;
|
||||
}
|
||||
if (auto value = FindConfigInt(document, "vr", "first_person_hidden_model");
|
||||
value && *value >= -1 && *value <= 31) {
|
||||
config.vrFirstPersonHiddenModel = static_cast<int32_t>(*value);
|
||||
@@ -880,6 +895,14 @@ inline bool SetVrFirstPersonHideDriver(bool value) {
|
||||
return WriteSetting("vr", "first_person_hide_driver", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonRotation(std::string value) {
|
||||
if (!IsSupportedVrFirstPersonRotation(value)) {
|
||||
return false;
|
||||
}
|
||||
Mutable().vrFirstPersonRotation = value;
|
||||
return WriteSetting("vr", "first_person_rotation", FormatString(value));
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonHiddenModel(int32_t value) {
|
||||
value = std::clamp(value, -1, 31);
|
||||
Mutable().vrFirstPersonHiddenModel = value;
|
||||
@@ -1180,6 +1203,11 @@ inline bool VrFirstPersonHideDriver(bool fallback = kVrFirstPersonHideDriverDefa
|
||||
return Get().vrFirstPersonHideDriver.value_or(fallback);
|
||||
}
|
||||
|
||||
inline std::string VrFirstPersonRotation(std::string fallback = kVrFirstPersonRotationDefault) {
|
||||
const auto& value = Get().vrFirstPersonRotation;
|
||||
return value && IsSupportedVrFirstPersonRotation(*value) ? *value : std::move(fallback);
|
||||
}
|
||||
|
||||
inline int32_t VrFirstPersonHiddenModel(int32_t fallback = kVrFirstPersonHiddenModelDefault) {
|
||||
return std::clamp(Get().vrFirstPersonHiddenModel.value_or(fallback), -1, 31);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,19 @@ struct FirstPersonHeadOffsets {
|
||||
float forward = 0.0f;
|
||||
};
|
||||
|
||||
// Where the anchored camera's orientation comes from, mirroring DolphinXR's
|
||||
// camera-anchor modes. The headset always adds free look on top of whichever
|
||||
// is chosen; this only decides the frame it looks around from.
|
||||
enum class FirstPersonRotation : uint8_t {
|
||||
// The horizon is kept level and only a heading is taken. Comfort default.
|
||||
YawOnly,
|
||||
// The kart's heading and its climb, with roll dropped: slopes and wheelies
|
||||
// tip the view, but a banked corner never rolls the horizon.
|
||||
YawPitch,
|
||||
// The kart's whole orientation, so the view banks and pitches with it.
|
||||
Full,
|
||||
};
|
||||
|
||||
// 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 {
|
||||
@@ -87,6 +100,32 @@ inline bool Normalize(Vec3& value) noexcept {
|
||||
return true;
|
||||
}
|
||||
|
||||
// out = matrix's 3x3 * (x, y, z). Directions ignore the translation column.
|
||||
inline Vec3 TransformDirection(const Mtx34& matrix, const Vec3& v) noexcept {
|
||||
return {
|
||||
matrix[0] * v.x + matrix[1] * v.y + matrix[2] * v.z,
|
||||
matrix[4] * v.x + matrix[5] * v.y + matrix[6] * v.z,
|
||||
matrix[8] * v.x + matrix[9] * v.y + matrix[10] * v.z,
|
||||
};
|
||||
}
|
||||
|
||||
// Fills the three basis rows from a forward and an up that need not be exactly
|
||||
// perpendicular, in the -Z-forward convention view space uses.
|
||||
inline bool BasisFromForwardUp(const Vec3& forward_in, const Vec3& up_in, Vec3 rows[3]) noexcept {
|
||||
Vec3 forward = forward_in;
|
||||
if (!Normalize(forward)) {
|
||||
return false;
|
||||
}
|
||||
Vec3 right = Cross(forward, up_in);
|
||||
if (!Normalize(right)) {
|
||||
return false;
|
||||
}
|
||||
rows[0] = right;
|
||||
rows[1] = Cross(right, forward);
|
||||
rows[2] = {-forward.x, -forward.y, -forward.z};
|
||||
return true;
|
||||
}
|
||||
|
||||
// out = matrix * (x, y, z, 1)
|
||||
inline Vec3 TransformPoint(const Mtx34& matrix, float x, float y, float z) noexcept {
|
||||
return {
|
||||
@@ -102,14 +141,12 @@ inline Vec3 TransformPoint(const Mtx34& matrix, float x, float y, float z) noexc
|
||||
// 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.
|
||||
// The translation always moves the camera onto the head; `rotation` decides the
|
||||
// frame it looks around from. 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,
|
||||
float head_forward_units, FirstPersonRotation rotation,
|
||||
Mtx34& out) noexcept {
|
||||
using namespace detail;
|
||||
if (!IsFiniteMtx34(view_from_world) || !IsFiniteMtx34(kart_from_local)) {
|
||||
@@ -124,39 +161,55 @@ inline bool ComputeFirstPersonAnchor(const Mtx34& view_from_world, const Mtx34&
|
||||
|
||||
// Rows of the anchor's rotation. Identity keeps the recorded camera's own
|
||||
// orientation and moves the eye only.
|
||||
// Every mode is the same construction from a forward and an up; they differ
|
||||
// only in which pair they take. Pairing a forward with world up is what
|
||||
// removes roll, since the resulting right axis is then always horizontal.
|
||||
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)) {
|
||||
// World +Y in view coordinates: the column of the view rotation that the
|
||||
// world up axis selects.
|
||||
Vec3 world_up{view_from_world[1], view_from_world[5], view_from_world[9]};
|
||||
const bool world_up_valid = Normalize(world_up);
|
||||
// Columns 2 and 1 of the kart pose are its forward and up. The pose may
|
||||
// carry scale, so the pair is re-orthonormalized rather than trusted.
|
||||
const Vec3 kart_forward = TransformDirection(
|
||||
view_from_world, {kart_from_local[2], kart_from_local[6], kart_from_local[10]});
|
||||
const Vec3 kart_up = TransformDirection(
|
||||
view_from_world, {kart_from_local[1], kart_from_local[5], kart_from_local[9]});
|
||||
|
||||
if (rotation == FirstPersonRotation::YawOnly) {
|
||||
if (!world_up_valid) {
|
||||
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};
|
||||
float along = Dot(camera_forward, world_up);
|
||||
Vec3 forward{camera_forward.x - world_up.x * along, camera_forward.y - world_up.y * along,
|
||||
camera_forward.z - world_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};
|
||||
along = Dot(camera_up, world_up);
|
||||
forward = {camera_up.x - world_up.x * along, camera_up.y - world_up.y * along,
|
||||
camera_up.z - world_up.z * along};
|
||||
if (!Normalize(forward)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Vec3 right = Cross(forward, up);
|
||||
if (!Normalize(right)) {
|
||||
if (!BasisFromForwardUp(forward, world_up, rows)) {
|
||||
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};
|
||||
} else if (rotation == FirstPersonRotation::YawPitch) {
|
||||
// The kart's heading and climb, levelled against world up so no roll
|
||||
// survives. Pointing straight up or down leaves nothing to level
|
||||
// against, so that frame falls back to the kart's own up.
|
||||
if (!world_up_valid || !BasisFromForwardUp(kart_forward, world_up, rows)) {
|
||||
if (!BasisFromForwardUp(kart_forward, kart_up, rows)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (!BasisFromForwardUp(kart_forward, kart_up, rows)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Mtx34 anchor{};
|
||||
@@ -181,7 +234,7 @@ inline bool ComputeFirstPersonAnchor(const Mtx34& view_from_world, const Mtx34&
|
||||
// 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;
|
||||
float units_per_meter, FirstPersonRotation rotation) 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
|
||||
|
||||
@@ -113,6 +113,16 @@ float g_vrFirstPersonHeadForward = RuntimeConfigFile::VrFirstPersonHeadForwardMe
|
||||
float g_vrFirstPersonHeadRight = RuntimeConfigFile::VrFirstPersonHeadRightMeters();
|
||||
bool g_vrFirstPersonHideDriver = RuntimeConfigFile::VrFirstPersonHideDriver();
|
||||
int g_vrFirstPersonHiddenModel = RuntimeConfigFile::VrFirstPersonHiddenModel();
|
||||
constexpr std::array<const char*, 3> kVrFirstPersonRotationNames{"yaw", "yaw_pitch", "full"};
|
||||
int g_vrFirstPersonRotation = [] {
|
||||
const std::string mode = RuntimeConfigFile::VrFirstPersonRotation();
|
||||
for (size_t i = 0; i < kVrFirstPersonRotationNames.size(); ++i) {
|
||||
if (mode == kVrFirstPersonRotationNames[i]) {
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}();
|
||||
// SDL_SCANCODE_UNKNOWN means unbound, which is also what an unrecognised
|
||||
// name in the config file resolves to rather than silently picking a key.
|
||||
SDL_Scancode g_vrRecenterScancode = [] {
|
||||
@@ -967,6 +977,21 @@ void DrawVrSettings() {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::TextDisabled("Where the head sits in the kart's own frame.");
|
||||
ImGui::PopTextWrapPos();
|
||||
constexpr std::array<const char*, 3> kRotationLabels{"Yaw only", "Yaw + Pitch", "Full rotation"};
|
||||
if (ImGui::Combo("View rotation", &g_vrFirstPersonRotation, kRotationLabels.data(),
|
||||
static_cast<int>(kRotationLabels.size()))) {
|
||||
RuntimeConfigFile::SetVrFirstPersonRotation(
|
||||
kVrFirstPersonRotationNames[static_cast<size_t>(g_vrFirstPersonRotation)]);
|
||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Where the view's orientation comes from. Yaw only keeps the horizon level "
|
||||
"and is the comfortable choice. Yaw + Pitch adds the kart's climb, so slopes "
|
||||
"and wheelies tip the view without ever rolling it. Full rotation takes the "
|
||||
"kart's whole orientation, banking included. The headset always adds free look "
|
||||
"on top.");
|
||||
}
|
||||
// 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) {
|
||||
@@ -1002,6 +1027,9 @@ void DrawVrSettings() {
|
||||
g_vrFirstPersonHeadRight = RuntimeConfigFile::kVrFirstPersonHeadRightDefault;
|
||||
g_vrFirstPersonHideDriver = RuntimeConfigFile::kVrFirstPersonHideDriverDefault;
|
||||
g_vrFirstPersonHiddenModel = RuntimeConfigFile::kVrFirstPersonHiddenModelDefault;
|
||||
g_vrFirstPersonRotation = 0;
|
||||
RuntimeConfigFile::SetVrFirstPersonRotation(
|
||||
RuntimeConfigFile::kVrFirstPersonRotationDefault);
|
||||
RuntimeConfigFile::SetVrFirstPersonUnitsPerMeter(g_vrFirstPersonUnitsPerMeter);
|
||||
RuntimeConfigFile::SetVrFirstPersonHeadUpMeters(g_vrFirstPersonHeadUp);
|
||||
RuntimeConfigFile::SetVrFirstPersonHeadForwardMeters(g_vrFirstPersonHeadForward);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
extern "C" void func_805A6C58(CpuContext* context);
|
||||
extern "C" void func_8056A470(CpuContext* context);
|
||||
@@ -236,6 +237,7 @@ struct FirstPersonState {
|
||||
bool enabled = false;
|
||||
FirstPersonHeadOffsets offsets{};
|
||||
float units_per_meter = RuntimeConfigFile::kVrFirstPersonUnitsPerMeterDefault;
|
||||
FirstPersonRotation rotation = FirstPersonRotation::YawOnly;
|
||||
|
||||
uint32_t camera_address = 0;
|
||||
// Armed by the draw boundary, consumed by the frame seal.
|
||||
@@ -494,10 +496,11 @@ void LogAnchorLocked(uint64_t frame, const Mtx34& anchor, const Mtx34& view_from
|
||||
} // namespace
|
||||
|
||||
void MkwVRFirstPersonConfigure(bool enabled, const FirstPersonHeadOffsets& offsets,
|
||||
float units_per_meter) noexcept {
|
||||
float units_per_meter, FirstPersonRotation rotation) noexcept {
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_state.enabled = enabled;
|
||||
g_state.offsets = offsets;
|
||||
g_state.rotation = rotation;
|
||||
if (detail::IsFiniteFloat(&units_per_meter) && units_per_meter > 0.0f) {
|
||||
g_state.units_per_meter = units_per_meter;
|
||||
}
|
||||
@@ -514,7 +517,12 @@ void MkwVRFirstPersonApplyConfiguredSettings() noexcept {
|
||||
RuntimeConfigFile::VrFirstPersonHeadUpMeters(),
|
||||
RuntimeConfigFile::VrFirstPersonHeadForwardMeters(),
|
||||
};
|
||||
MkwVRFirstPersonConfigure(RuntimeConfigFile::VrFirstPerson(false), offsets, units_per_meter);
|
||||
const std::string mode = RuntimeConfigFile::VrFirstPersonRotation();
|
||||
const FirstPersonRotation rotation = mode == "full" ? FirstPersonRotation::Full
|
||||
: mode == "yaw_pitch" ? FirstPersonRotation::YawPitch
|
||||
: FirstPersonRotation::YawOnly;
|
||||
MkwVRFirstPersonConfigure(RuntimeConfigFile::VrFirstPerson(false), offsets, units_per_meter,
|
||||
rotation);
|
||||
MkwVRPolicySetFirstPersonUnitsPerMeter(units_per_meter);
|
||||
{
|
||||
// Same lock the guest thread applies these under.
|
||||
@@ -589,7 +597,7 @@ void MkwVRFirstPersonCommit() noexcept {
|
||||
g_state.offsets.right * g_state.units_per_meter,
|
||||
g_state.offsets.up * g_state.units_per_meter,
|
||||
g_state.offsets.forward * g_state.units_per_meter,
|
||||
/*level_horizon=*/true, anchor)) {
|
||||
g_state.rotation, anchor)) {
|
||||
failed_step = "anchor math (degenerate camera or kart frame)";
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace {
|
||||
|
||||
using mkw::vr::ComputeFirstPersonAnchor;
|
||||
using mkw::vr::kIdentityMtx34;
|
||||
using mkw::vr::FirstPersonRotation;
|
||||
using mkw::vr::Mtx34;
|
||||
|
||||
int g_failures = 0;
|
||||
@@ -77,20 +78,23 @@ Mtx34 KartAt(float x, float y, float z) {
|
||||
void TestNeutralInputsProduceIdentity() {
|
||||
Mtx34 anchor{};
|
||||
Check(ComputeFirstPersonAnchor(kIdentityMtx34, kIdentityMtx34, 0.0f, 0.0f, 0.0f,
|
||||
/*level_horizon=*/true, anchor),
|
||||
FirstPersonRotation::YawOnly, anchor),
|
||||
"a camera already at the head must produce an anchor");
|
||||
for (size_t i = 0; i < anchor.size(); ++i) {
|
||||
CheckNear(anchor[i], kIdentityMtx34[i], "neutral inputs must produce the identity anchor");
|
||||
}
|
||||
}
|
||||
|
||||
void TestUnlevelledAnchorIsPureTranslation() {
|
||||
void TestLevelCameraGivesPureTranslation() {
|
||||
// Camera 5 m behind and 2 m above the origin, kart at the origin, head 1 m up.
|
||||
// A camera that is already level needs no rotation, so the anchor reduces to
|
||||
// the translation and the head-placement math is visible on its own.
|
||||
const Mtx34 view = LevelViewAt(0.0f, 2.0f, 5.0f);
|
||||
const Mtx34 kart = KartAt(0.0f, 0.0f, 0.0f);
|
||||
Mtx34 anchor{};
|
||||
Check(ComputeFirstPersonAnchor(view, kart, 0.0f, 1.0f, 0.0f, /*level_horizon=*/false, anchor),
|
||||
"an unlevelled anchor must be computable");
|
||||
Check(ComputeFirstPersonAnchor(view, kart, 0.0f, 1.0f, 0.0f, FirstPersonRotation::YawOnly,
|
||||
anchor),
|
||||
"a level camera must produce an anchor");
|
||||
|
||||
// The head sits at (0, -1, -5) in view space, so the anchor's translation
|
||||
// is its negation.
|
||||
@@ -113,7 +117,7 @@ void TestLevellingRemovesCameraPitch() {
|
||||
const Mtx34 view = PitchedViewAt(0.0f, 2.0f, 5.0f, pitch);
|
||||
const Mtx34 kart = KartAt(0.0f, 0.0f, 0.0f);
|
||||
Mtx34 anchor{};
|
||||
Check(ComputeFirstPersonAnchor(view, kart, 0.0f, 1.0f, 0.0f, /*level_horizon=*/true, anchor),
|
||||
Check(ComputeFirstPersonAnchor(view, kart, 0.0f, 1.0f, 0.0f, FirstPersonRotation::YawOnly, anchor),
|
||||
"a pitched camera must still produce an anchor");
|
||||
|
||||
// The anchored camera's axes, expressed in world space: rows of A_rot times
|
||||
@@ -157,7 +161,7 @@ void TestAnchorRotationStaysOrthonormal() {
|
||||
const Mtx34 view = PitchedViewAt(3.0f, 12.0f, -7.0f, pitch);
|
||||
Mtx34 anchor{};
|
||||
Check(ComputeFirstPersonAnchor(view, KartAt(3.0f, 0.0f, -20.0f), 0.1f, 1.0f, 0.2f,
|
||||
/*level_horizon=*/true, anchor),
|
||||
FirstPersonRotation::YawOnly, anchor),
|
||||
"every camera pitch must produce an anchor");
|
||||
for (size_t row = 0; row < 3; ++row) {
|
||||
for (size_t other = row; other < 3; ++other) {
|
||||
@@ -177,7 +181,7 @@ void TestNonFiniteInputIsRejected() {
|
||||
broken[3] = std::numeric_limits<float>::infinity();
|
||||
Mtx34 anchor = kIdentityMtx34;
|
||||
anchor[3] = 1234.0f;
|
||||
Check(!ComputeFirstPersonAnchor(broken, kIdentityMtx34, 0.0f, 1.0f, 0.0f, true, anchor),
|
||||
Check(!ComputeFirstPersonAnchor(broken, kIdentityMtx34, 0.0f, 1.0f, 0.0f, FirstPersonRotation::YawOnly, anchor),
|
||||
"a non-finite view matrix must be rejected");
|
||||
CheckNear(anchor[3], 1234.0f, "a rejected anchor must leave the output untouched");
|
||||
}
|
||||
@@ -186,17 +190,130 @@ void TestDegenerateKartPoseIsRejected() {
|
||||
Mtx34 collapsed{};
|
||||
Mtx34 anchor{};
|
||||
// A zeroed view matrix has no world up to level against.
|
||||
Check(!ComputeFirstPersonAnchor(collapsed, kIdentityMtx34, 0.0f, 1.0f, 0.0f, true, anchor),
|
||||
Check(!ComputeFirstPersonAnchor(collapsed, kIdentityMtx34, 0.0f, 1.0f, 0.0f, FirstPersonRotation::YawOnly, anchor),
|
||||
"a collapsed view matrix must be rejected");
|
||||
}
|
||||
|
||||
// A kart pitched up by `pitch` and rolled by `roll`, heading toward -Z so it
|
||||
// points away from a level camera. Only columns 1 and 2 are read by the anchor.
|
||||
Mtx34 KartPitchedAndRolled(float pitch, float roll) {
|
||||
const float cp = std::cos(pitch), sp = std::sin(pitch);
|
||||
const float cr = std::cos(roll), sr = std::sin(roll);
|
||||
const float forward[3]{0.0f, sp, -cp};
|
||||
const float upUnrolled[3]{0.0f, cp, sp};
|
||||
const float rightUnrolled[3]{1.0f, 0.0f, 0.0f};
|
||||
Mtx34 pose{};
|
||||
for (size_t row = 0; row < 3; ++row) {
|
||||
pose[row * 4 + 1] = -rightUnrolled[row] * sr + upUnrolled[row] * cr;
|
||||
pose[row * 4 + 2] = forward[row];
|
||||
}
|
||||
return pose;
|
||||
}
|
||||
|
||||
void TestYawPitchKeepsClimbAndDropsRoll() {
|
||||
const float pitch = 0.4f, roll = 0.5f;
|
||||
const Mtx34 view = LevelViewAt(0.0f, 2.0f, 5.0f);
|
||||
Mtx34 anchor{};
|
||||
Check(ComputeFirstPersonAnchor(view, KartPitchedAndRolled(pitch, roll), 0.0f, 0.0f, 0.0f,
|
||||
FirstPersonRotation::YawPitch, anchor),
|
||||
"yaw+pitch must be computable");
|
||||
|
||||
// The climb survives: the anchor's forward is the kart's forward.
|
||||
CheckNear(-anchor[9], std::sin(pitch), "yaw+pitch keeps the kart's climb (y)");
|
||||
CheckNear(-anchor[10], -std::cos(pitch), "yaw+pitch keeps the kart's heading (z)");
|
||||
// The roll does not: the right axis stays horizontal.
|
||||
CheckNear(anchor[1], 0.0f, "yaw+pitch leaves the right axis horizontal");
|
||||
|
||||
// Full rotation on the same kart does keep the roll, so the two differ.
|
||||
Mtx34 full{};
|
||||
Check(ComputeFirstPersonAnchor(view, KartPitchedAndRolled(pitch, roll), 0.0f, 0.0f, 0.0f,
|
||||
FirstPersonRotation::Full, full),
|
||||
"full rotation must be computable");
|
||||
Check(std::fabs(full[1]) > 0.1f, "full rotation keeps the roll yaw+pitch drops");
|
||||
|
||||
for (size_t row = 0; row < 3; ++row) {
|
||||
for (size_t other = row; other < 3; ++other) {
|
||||
float dot = 0.0f;
|
||||
for (size_t axis = 0; axis < 3; ++axis) {
|
||||
dot += anchor[row * 4 + axis] * anchor[other * 4 + axis];
|
||||
}
|
||||
CheckNear(dot, row == other ? 1.0f : 0.0f, "yaw+pitch stays orthonormal");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A kart yawed by `yaw` and rolled by `roll`, as a kart-local -> world pose.
|
||||
Mtx34 KartOriented(float yaw, float roll) {
|
||||
const float cy = std::cos(yaw), sy = std::sin(yaw);
|
||||
const float cr = std::cos(roll), sr = std::sin(roll);
|
||||
// Columns are the kart's right, up and forward axes in world space.
|
||||
const float right[3]{cy * cr, sr, -sy * cr};
|
||||
const float up[3]{-cy * sr, cr, sy * sr};
|
||||
const float forward[3]{sy, 0.0f, cy};
|
||||
Mtx34 pose{};
|
||||
for (size_t row = 0; row < 3; ++row) {
|
||||
pose[row * 4 + 0] = right[row];
|
||||
pose[row * 4 + 1] = up[row];
|
||||
pose[row * 4 + 2] = forward[row];
|
||||
}
|
||||
return pose;
|
||||
}
|
||||
|
||||
void TestFullRotationFollowsTheKart() {
|
||||
// A level camera, and a kart yawed and rolled away from it. Full rotation
|
||||
// must adopt the kart's frame, not the camera's.
|
||||
const Mtx34 view = LevelViewAt(0.0f, 2.0f, 5.0f);
|
||||
const float yaw = 0.6f, roll = 0.4f;
|
||||
Mtx34 anchor{};
|
||||
Check(ComputeFirstPersonAnchor(view, KartOriented(yaw, roll), 0.0f, 0.0f, 0.0f,
|
||||
FirstPersonRotation::Full, anchor),
|
||||
"full rotation must be computable");
|
||||
|
||||
// With an identity view rotation the anchor rows are the kart's axes
|
||||
// directly, so the third row is the kart's backward axis.
|
||||
CheckNear(anchor[8], -std::sin(yaw), "full rotation takes the kart's heading (x)");
|
||||
CheckNear(anchor[10], -std::cos(yaw), "full rotation takes the kart's heading (z)");
|
||||
// Roll survives: the anchor's up is the kart's up, not world up.
|
||||
CheckNear(anchor[5], std::cos(roll), "full rotation keeps the kart's roll");
|
||||
|
||||
for (size_t row = 0; row < 3; ++row) {
|
||||
for (size_t other = row; other < 3; ++other) {
|
||||
float dot = 0.0f;
|
||||
for (size_t axis = 0; axis < 3; ++axis) {
|
||||
dot += anchor[row * 4 + axis] * anchor[other * 4 + axis];
|
||||
}
|
||||
CheckNear(dot, row == other ? 1.0f : 0.0f, "full rotation stays orthonormal");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestYawOnlyIgnoresKartRoll() {
|
||||
// The same rolled kart, but yaw-only must leave the horizon level.
|
||||
const Mtx34 view = LevelViewAt(0.0f, 2.0f, 5.0f);
|
||||
Mtx34 rolled{};
|
||||
Mtx34 upright{};
|
||||
Check(ComputeFirstPersonAnchor(view, KartOriented(0.6f, 0.4f), 0.0f, 0.0f, 0.0f,
|
||||
FirstPersonRotation::YawOnly, rolled),
|
||||
"yaw-only must be computable for a rolled kart");
|
||||
Check(ComputeFirstPersonAnchor(view, KartOriented(0.6f, 0.0f), 0.0f, 0.0f, 0.0f,
|
||||
FirstPersonRotation::YawOnly, upright),
|
||||
"yaw-only must be computable for an upright kart");
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
CheckNear(rolled[4 + i], upright[4 + i], "yaw-only ignores the kart's roll");
|
||||
}
|
||||
CheckNear(rolled[5], 1.0f, "yaw-only keeps the horizon level");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestNeutralInputsProduceIdentity();
|
||||
TestUnlevelledAnchorIsPureTranslation();
|
||||
TestLevelCameraGivesPureTranslation();
|
||||
TestLevellingRemovesCameraPitch();
|
||||
TestAnchorRotationStaysOrthonormal();
|
||||
TestFullRotationFollowsTheKart();
|
||||
TestYawPitchKeepsClimbAndDropsRoll();
|
||||
TestYawOnlyIgnoresKartRoll();
|
||||
TestNonFiniteInputIsRejected();
|
||||
TestDegenerateKartPoseIsRejected();
|
||||
if (g_failures != 0) {
|
||||
|
||||
Reference in new issue
Block a user