diff --git a/src/mount.cpp b/src/mount.cpp index 8de84f0..9244c61 100644 --- a/src/mount.cpp +++ b/src/mount.cpp @@ -62,19 +62,31 @@ std::optional world_mount_pose(const Matrix34& hmd) { return pose; } -Matrix34 relative_mount_pose(Mount mount) { +Matrix34 relative_mount_pose(Mount mount, const WristPlacement& wrist) { auto pose = identity; switch (mount) { case Mount::Head: pose[1][3] = -0.16f; pose[2][3] = -1.05f; break; - case Mount::LeftWrist: pose[0][3] = 0.13f; pose[1][3] = 0.12f; pose[2][3] = -0.18f; break; - case Mount::RightWrist: pose[0][3] = -0.13f; pose[1][3] = 0.12f; pose[2][3] = -0.18f; break; + case Mount::LeftWrist: case Mount::RightWrist: { + // VR Workspace's fallback wrist frame maps +Y toward fingers (-Z of + // controller), +Z out of the back of the hand (+Y of controller). + // Its HUD has right=fingers, up=back of hand, front=right x up. + // The 0.089 m Z is the 0.054 m wrist calibration plus 0.035 m + // backward from the fingers. Y also accounts for the HUD canvas's + // bottom anchor: .12 m wrist lift + .09 m anchor - ~.03 m panel center. + const float angle = wrist.roll_degrees * (std::acos(-1.f) / 180.f); + const float c = std::cos(angle), s = std::sin(angle); + pose[0] = {0.f, s, c, c * wrist.x + s * wrist.y}; + pose[1] = {0.f, c, -s, -s * wrist.x + c * wrist.y}; + pose[2] = {-1.f, 0.f, 0.f, wrist.z}; + break; + } case Mount::World: break; } return pose; } -float mount_width(Mount mount) { - return mount == Mount::LeftWrist || mount == Mount::RightWrist ? 0.42f : 0.85f; +float mount_width(Mount mount, const WristPlacement& wrist) { + return mount == Mount::LeftWrist || mount == Mount::RightWrist ? wrist.width : 0.85f; } std::filesystem::path default_mount_settings_path() { diff --git a/src/mount.hpp b/src/mount.hpp index 6cffb3d..02eed9e 100644 --- a/src/mount.hpp +++ b/src/mount.hpp @@ -15,12 +15,20 @@ std::optional parse_mount(std::string_view name); // Row-major 3x4 affine transform (OpenVR axes: +Y up, -Z forward). using Matrix34 = std::array, 3>; +// Controller-local watch placement. Defaults follow VR Workspace's fallback +// wrist calibration and HUD pose (not its avatar-specific calibration). +struct WristPlacement { + float x = 0.f, y = 0.18f, z = 0.089f; // meters in controller coordinates + float width = 0.30f; // meters + float roll_degrees = 0.f; // about controller -Z before placing the panel +}; + // World-space, yaw-only pose in front of the HMD, upright and facing the viewer. // Invalid/nonfinite HMD transforms or a vanishing horizontal forward return nullopt. std::optional world_mount_pose(const Matrix34& hmd); // Device-relative transform; World has no relative anchor and returns identity. -Matrix34 relative_mount_pose(Mount mount); -float mount_width(Mount mount); +Matrix34 relative_mount_pose(Mount mount, const WristPlacement& wrist = {}); +float mount_width(Mount mount, const WristPlacement& wrist = {}); // Empty when neither XDG_CONFIG_HOME nor HOME specifies an absolute base. std::filesystem::path default_mount_settings_path(); diff --git a/tests/mount_test.cpp b/tests/mount_test.cpp index aba354d..79b8b02 100644 --- a/tests/mount_test.cpp +++ b/tests/mount_test.cpp @@ -60,16 +60,25 @@ int main() { CHECK(!parse_mount("")); CHECK(!parse_mount("world extra")); CHECK(near(mount_width(Mount::World), .85f)); CHECK(near(mount_width(Mount::Head), .85f)); - CHECK(near(mount_width(Mount::LeftWrist), .42f)); CHECK(near(mount_width(Mount::RightWrist), .42f)); + CHECK(near(mount_width(Mount::LeftWrist), .30f)); CHECK(near(mount_width(Mount::RightWrist), .30f)); CHECK(relative_mount_pose(Mount::World) == identity); const auto head = relative_mount_pose(Mount::Head); check_basis(head, 1, 0, 0, 1); CHECK(near(head[0][3], 0)); CHECK(near(head[1][3], -.16f)); CHECK(near(head[2][3], -1.05f)); const auto left = relative_mount_pose(Mount::LeftWrist), right = relative_mount_pose(Mount::RightWrist); - check_basis(left, 1, 0, 0, 1); check_basis(right, 1, 0, 0, 1); - CHECK(near(left[0][3], .13f)); CHECK(near(right[0][3], -.13f)); - CHECK(near(left[1][3], .12f)); CHECK(near(right[1][3], .12f)); - CHECK(near(left[2][3], -.18f)); CHECK(near(right[2][3], -.18f)); + CHECK(left == right); // VR Workspace fallback calibration uses the same controller-local axes for both hands. + CHECK(near(left[0][0], 0)); CHECK(near(left[1][0], 0)); CHECK(near(left[2][0], -1)); + CHECK(near(left[0][1], 0)); CHECK(near(left[1][1], 1)); CHECK(near(left[2][1], 0)); + CHECK(near(left[0][2], 1)); CHECK(near(left[1][2], 0)); CHECK(near(left[2][2], 0)); + CHECK(near(left[0][3], 0)); CHECK(near(left[1][3], .18f)); CHECK(near(left[2][3], .089f)); + WristPlacement tuned{.02f, .1f, .07f, .25f, 90.f}; + const auto rotated = relative_mount_pose(Mount::RightWrist, tuned); + CHECK(near(rotated[0][1], 1)); CHECK(near(rotated[1][1], 0)); + CHECK(near(rotated[0][2], 0)); CHECK(near(rotated[1][2], -1)); + CHECK(near(rotated[0][3], .1f)); CHECK(near(rotated[1][3], -.02f)); + CHECK(near(rotated[2][3], .07f)); CHECK(near(mount_width(Mount::RightWrist, tuned), .25f)); + CHECK(relative_mount_pose(Mount::Head, tuned) == head); + CHECK(near(mount_width(Mount::World, tuned), .85f)); auto hmd = identity; hmd[0][3] = 2; hmd[1][3] = 1.7f; hmd[2][3] = 3;