mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 05:00:27 +02:00
Implement OpenXR Wii Remote support
This commit is contained in:
1 parent
75f0f5f26d
commit
19485b9e8b
18 files changed
+1622
-93
No files matched your search
@@ -0,0 +1,293 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// The VR controllers' Wii Remote presentation, tested without a headset: the
|
||||
// KPAD accelerometer frame, the absolute pointer against a virtual screen, the
|
||||
// picture's place on the menu quad, the off-screen debounce and the button
|
||||
// profile. Every expectation is stated in the Wii's own terms, so a sign error
|
||||
// in the geometry shows up as the wrong button face, tilt or cursor edge.
|
||||
|
||||
#include "vr/openxr_wii_remote.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace mkw::vr::wii_remote;
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
void Check(bool condition, const char* what) {
|
||||
if (!condition) {
|
||||
++g_failures;
|
||||
std::cerr << "FAILED: " << what << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void CheckNear(float actual, float expected, const char* what, float tolerance = 1.0e-3f) {
|
||||
if (!(std::fabs(actual - expected) <= tolerance)) {
|
||||
++g_failures;
|
||||
std::cerr << "FAILED: " << what << " (expected " << expected << ", got " << actual << ")\n";
|
||||
}
|
||||
}
|
||||
|
||||
constexpr float kHalfTurn = 3.14159265f;
|
||||
constexpr float kQuarterTurn = 0.5f * kHalfTurn;
|
||||
|
||||
Quat AxisAngle(float x, float y, float z, float radians) {
|
||||
const float s = std::sin(0.5f * radians);
|
||||
return {x * s, y * s, z * s, std::cos(0.5f * radians)};
|
||||
}
|
||||
|
||||
// Aim poses as a player holds the controller. Forward is -Z, up +Y, right +X.
|
||||
constexpr Quat kLevel{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
Quat PitchedUp(float radians) { return AxisAngle(1.0f, 0.0f, 0.0f, radians); }
|
||||
Quat YawedLeft(float radians) { return AxisAngle(0.0f, 1.0f, 0.0f, radians); }
|
||||
// Seen from behind the controller, looking where it points.
|
||||
Quat RolledClockwise(float radians) { return AxisAngle(0.0f, 0.0f, 1.0f, -radians); }
|
||||
|
||||
void TestRestingAccelerometer() {
|
||||
const Vec3 zero{};
|
||||
// KPAD's rest reading with the buttons up.
|
||||
Vec3 acc = KpadAcceleration(kLevel, zero);
|
||||
CheckNear(acc[0], 0.0f, "level remote: x");
|
||||
CheckNear(acc[1], -1.0f, "level remote: gravity through the back of the remote");
|
||||
CheckNear(acc[2], 0.0f, "level remote: z");
|
||||
|
||||
// Pointing at the floor, the remote's back end (KPAD +z, towards the
|
||||
// player) is the side facing up.
|
||||
acc = KpadAcceleration(PitchedUp(-kQuarterTurn), zero);
|
||||
CheckNear(acc[1], 0.0f, "pointing down: y");
|
||||
CheckNear(acc[2], 1.0f, "pointing down: gravity along z");
|
||||
|
||||
// Pointing at the ceiling it is the tip that faces up.
|
||||
acc = KpadAcceleration(PitchedUp(kQuarterTurn), zero);
|
||||
CheckNear(acc[2], -1.0f, "pointing up: gravity along -z");
|
||||
|
||||
// Rolled a quarter turn clockwise the remote's left edge (KPAD -x) faces up.
|
||||
acc = KpadAcceleration(RolledClockwise(kQuarterTurn), zero);
|
||||
CheckNear(acc[0], -1.0f, "rolled clockwise: gravity along -x");
|
||||
CheckNear(acc[1], 0.0f, "rolled clockwise: y");
|
||||
|
||||
// Buttons facing the floor.
|
||||
acc = KpadAcceleration(RolledClockwise(kHalfTurn), zero);
|
||||
CheckNear(acc[1], 1.0f, "upside down: gravity along +y");
|
||||
}
|
||||
|
||||
void TestAccelerometerRange() {
|
||||
// A 10 g upward jolt saturates like the ADXL330 instead of reporting 11 g.
|
||||
const Vec3 acc = KpadAcceleration(kLevel, {0.0f, 10.0f * kStandardGravity, 0.0f});
|
||||
CheckNear(acc[1], -kAccelRangeG, "saturates at the sensor's range");
|
||||
}
|
||||
|
||||
void TestMotionTracker() {
|
||||
MotionTracker tracker;
|
||||
const Quat orientation = kLevel;
|
||||
// Rising at 1 g: p = a t^2 / 2, v = a t, sampled at 90 Hz.
|
||||
const float a = kStandardGravity;
|
||||
const int64_t step_ns = 11'111'111;
|
||||
Vec3 acc{};
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
const float t = static_cast<float>(i) * static_cast<float>(step_ns) * 1.0e-9f;
|
||||
const Vec3 position{0.0f, 0.5f * a * t * t, 0.0f};
|
||||
const Vec3 velocity{0.0f, a * t, 0.0f};
|
||||
acc = tracker.Update(&orientation, &position, &velocity, i * step_ns);
|
||||
if (i == 0) {
|
||||
CheckNear(acc[1], -1.0f, "first sample has no history: gravity only");
|
||||
}
|
||||
}
|
||||
// Gravity plus the climb: 2 g through the back of the remote.
|
||||
CheckNear(acc[1], -2.0f, "steady 1 g climb reads 2 g", 1.0e-2f);
|
||||
|
||||
// Holding still again settles back to rest.
|
||||
const Vec3 still_position{0.0f, 1.0f, 0.0f};
|
||||
const Vec3 still_velocity{};
|
||||
tracker.Reset();
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
acc = tracker.Update(&orientation, &still_position, &still_velocity, (10 + i) * step_ns);
|
||||
}
|
||||
CheckNear(acc[1], -1.0f, "at rest after a reset", 1.0e-3f);
|
||||
|
||||
// Losing tracking repeats the last reading instead of inventing one.
|
||||
const Vec3 held = tracker.Update(nullptr, nullptr, nullptr, 20 * step_ns);
|
||||
CheckNear(held[1], acc[1], "untracked controller holds its last reading");
|
||||
}
|
||||
|
||||
Screen ScreenAhead(float distance, float half_width, float half_height) {
|
||||
Screen screen;
|
||||
screen.pose.position = {0.0f, 0.0f, -distance};
|
||||
screen.half_width = half_width;
|
||||
screen.half_height = half_height;
|
||||
return screen;
|
||||
}
|
||||
|
||||
void TestRaycast() {
|
||||
const Screen screen = ScreenAhead(2.0f, 1.2f, 0.675f);
|
||||
|
||||
Pose aim;
|
||||
ScreenHit hit = RaycastScreen(aim, screen);
|
||||
Check(hit.valid, "aiming at the screen's centre hits");
|
||||
CheckNear(hit.u, 0.0f, "centre: u");
|
||||
CheckNear(hit.v, 0.0f, "centre: v");
|
||||
CheckNear(hit.distance_meters, 2.0f, "centre: distance");
|
||||
|
||||
// Absolute: sliding the hand to the right edge puts the pointer there.
|
||||
aim.position = {1.2f, 0.0f, 0.0f};
|
||||
hit = RaycastScreen(aim, screen);
|
||||
CheckNear(hit.u, 1.0f, "hand at the right edge: u");
|
||||
|
||||
// Tilting up to the top edge: KPAD's y is -1 at the top.
|
||||
aim.position = {};
|
||||
aim.orientation = PitchedUp(std::atan(0.675f / 2.0f));
|
||||
hit = RaycastScreen(aim, screen);
|
||||
CheckNear(hit.v, 1.0f, "tilted to the top edge: v");
|
||||
CheckNear(KpadPosition(hit)[1], -1.0f, "top edge is KPAD y -1");
|
||||
|
||||
// Turning left moves the pointer left.
|
||||
aim.orientation = YawedLeft(std::atan(0.6f / 2.0f));
|
||||
hit = RaycastScreen(aim, screen);
|
||||
CheckNear(hit.u, -0.5f, "turned left: u");
|
||||
CheckNear(KpadPosition(hit)[0], -0.5f, "turned left: KPAD x");
|
||||
|
||||
// Rotating the controller does not change the distance.
|
||||
CheckNear(hit.distance_meters, 2.0f, "distance is perpendicular");
|
||||
|
||||
// Pointing away, or standing behind the screen, is no hit at all.
|
||||
aim.orientation = YawedLeft(kHalfTurn);
|
||||
Check(!RaycastScreen(aim, screen).valid, "pointing away misses");
|
||||
aim.orientation = kLevel;
|
||||
aim.position = {0.0f, 0.0f, -3.0f};
|
||||
Check(!RaycastScreen(aim, screen).valid, "behind the screen misses");
|
||||
|
||||
// A screen off to the side, facing the player: only the pose matters.
|
||||
Screen side;
|
||||
side.pose.position = {-2.0f, 0.0f, 0.0f};
|
||||
side.pose.orientation = YawedLeft(kQuarterTurn); // its +Z faces +X, back at the player
|
||||
side.half_width = 1.0f;
|
||||
side.half_height = 1.0f;
|
||||
Pose towards_side;
|
||||
towards_side.orientation = YawedLeft(kQuarterTurn); // aim -Z becomes -X
|
||||
hit = RaycastScreen(towards_side, side);
|
||||
Check(hit.valid, "turned towards a side screen hits");
|
||||
CheckNear(hit.u, 0.0f, "side screen: u");
|
||||
towards_side.position = {0.0f, 0.5f, 0.0f};
|
||||
hit = RaycastScreen(towards_side, side);
|
||||
CheckNear(hit.v, 0.5f, "side screen: raised hand raises the pointer");
|
||||
}
|
||||
|
||||
void TestHorizon() {
|
||||
const Screen screen = ScreenAhead(2.0f, 1.0f, 1.0f);
|
||||
Pose aim;
|
||||
std::array<float, 2> horizon = Horizon(aim, screen);
|
||||
CheckNear(horizon[0], 1.0f, "level remote: horizon x");
|
||||
CheckNear(horizon[1], 0.0f, "level remote: horizon y");
|
||||
// The SDK's (0, 1) for a quarter turn clockwise.
|
||||
aim.orientation = RolledClockwise(kQuarterTurn);
|
||||
horizon = Horizon(aim, screen);
|
||||
CheckNear(horizon[0], 0.0f, "rolled clockwise: horizon x");
|
||||
CheckNear(horizon[1], 1.0f, "rolled clockwise: horizon y");
|
||||
}
|
||||
|
||||
void TestPointerFilter() {
|
||||
PointerFilter filter;
|
||||
const int64_t ms = 1'000'000;
|
||||
ScreenHit on{true, 0.25f, -0.5f, 1.5f};
|
||||
ScreenHit result = filter.Update(on, 0);
|
||||
Check(result.valid && result.u == 0.25f, "on screen passes through");
|
||||
|
||||
// A tracking blip holds the last position.
|
||||
result = filter.Update(ScreenHit{}, 10 * ms);
|
||||
Check(result.valid && result.u == 0.25f, "lost hit holds the pointer");
|
||||
// An excursion past the margin pins at the margin.
|
||||
result = filter.Update(ScreenHit{true, 3.0f, 0.0f, 1.5f}, 50 * ms);
|
||||
Check(result.valid, "short excursion stays visible");
|
||||
CheckNear(result.u, kPointerMarginU, "short excursion pins at the margin");
|
||||
// Sustained, it is hidden like a remote that lost the sensor bar.
|
||||
result = filter.Update(ScreenHit{true, 3.0f, 0.0f, 1.5f}, 110 * ms);
|
||||
Check(!result.valid, "sustained excursion hides the pointer");
|
||||
result = filter.Update(ScreenHit{}, 120 * ms);
|
||||
Check(!result.valid, "stays hidden");
|
||||
// Coming back shows it straight away.
|
||||
result = filter.Update(on, 130 * ms);
|
||||
Check(result.valid, "returning to the screen shows it again");
|
||||
|
||||
// Never having been on screen, a lost hit is simply no pointer.
|
||||
PointerFilter fresh;
|
||||
Check(!fresh.Update(ScreenHit{}, 0).valid, "no pointer before the first hit");
|
||||
// Just inside the margins still counts as on screen.
|
||||
Check(fresh.Update(ScreenHit{true, 1.8f, -1.4f, 1.0f}, ms).valid, "inside the margins is tracked");
|
||||
}
|
||||
|
||||
void TestMenuPicture() {
|
||||
// 16:9 game in a 16:9 window on a square eye texture: full width.
|
||||
std::array<float, 2> extents = MenuPictureHalfExtents(2.4f, 1.0f, 16.0f / 9.0f, 16.0f / 9.0f);
|
||||
CheckNear(extents[0], 1.2f, "16:9 picture: half width");
|
||||
CheckNear(extents[1], 0.675f, "16:9 picture: half height");
|
||||
// 4:3 game pillarboxed inside that window.
|
||||
extents = MenuPictureHalfExtents(2.4f, 1.0f, 16.0f / 9.0f, 4.0f / 3.0f);
|
||||
CheckNear(extents[0], 0.9f, "4:3 picture in 16:9 window: half width");
|
||||
CheckNear(extents[1], 0.675f, "4:3 picture in 16:9 window: half height");
|
||||
// A wide eye texture pillarboxes a square snapshot by width instead.
|
||||
extents = MenuPictureHalfExtents(2.0f, 2.0f, 1.0f, 1.0f);
|
||||
CheckNear(extents[0], 0.5f, "square picture on a wide quad: half width");
|
||||
CheckNear(extents[1], 0.5f, "square picture on a wide quad: half height");
|
||||
}
|
||||
|
||||
void TestButtons() {
|
||||
HandInputs left;
|
||||
HandInputs right;
|
||||
Check(RemoteButtons(left, right) == 0, "nothing held");
|
||||
|
||||
right.primary = true;
|
||||
right.trigger = 0.6f;
|
||||
Check(RemoteButtons(left, right) == (kButtonA | kButtonB), "right A and trigger are A and B");
|
||||
|
||||
right = {};
|
||||
right.stick_y = 0.9f;
|
||||
Check(RemoteButtons(left, right) == kButtonOne, "right stick up is 1");
|
||||
right.stick_y = -0.9f;
|
||||
Check(RemoteButtons(left, right) == kButtonTwo, "right stick down is 2");
|
||||
right.stick_y = 0.0f;
|
||||
right.stick_x = -0.9f;
|
||||
Check(RemoteButtons(left, right) == kButtonMinus, "right stick left is -");
|
||||
right.stick_x = 0.9f;
|
||||
Check(RemoteButtons(left, right) == kButtonPlus, "right stick right is +");
|
||||
right.stick_x = 0.3f;
|
||||
Check(RemoteButtons(left, right) == 0, "a light push is no press");
|
||||
|
||||
right = {};
|
||||
left.menu = true;
|
||||
left.squeeze = 0.8f;
|
||||
left.trigger = 0.7f;
|
||||
Check(RemoteButtons(left, right) == (kButtonHome | kButtonC | kButtonZ), "left menu, grip, trigger are HOME, C, Z");
|
||||
// Left X and Y have no Wii button in the profile.
|
||||
left = {};
|
||||
left.primary = true;
|
||||
left.secondary = true;
|
||||
Check(RemoteButtons(left, right) == 0, "left X/Y are unbound");
|
||||
|
||||
left.stick_x = 1.0f;
|
||||
left.stick_y = 1.0f;
|
||||
const std::array<float, 2> stick = NunchukStick(left);
|
||||
CheckNear(std::hypot(stick[0], stick[1]), 1.0f, "diagonal stays inside the gate");
|
||||
CheckNear(stick[0], stick[1], "diagonal keeps its direction");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestRestingAccelerometer();
|
||||
TestAccelerometerRange();
|
||||
TestMotionTracker();
|
||||
TestRaycast();
|
||||
TestHorizon();
|
||||
TestPointerFilter();
|
||||
TestMenuPicture();
|
||||
TestButtons();
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " check(s) failed\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "vr_wii_remote_tests: all checks passed\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user