Match wrist mount geometry to VR Workspace watch panel

This commit is contained in:
baketnk committed 2026-09-24 14:44:59 -04:00
1 parent e5cb80ce59
commit 569ba13830
3 files changed
+41 -12

No files matched your search

+17 -5
View File
@@ -62,19 +62,31 @@ std::optional<Matrix34> 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() {
+10 -2
View File
@@ -15,12 +15,20 @@ std::optional<Mount> parse_mount(std::string_view name);
// Row-major 3x4 affine transform (OpenVR axes: +Y up, -Z forward).
using Matrix34 = std::array<std::array<float, 4>, 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<Matrix34> 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();
+14 -5
View File
@@ -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;