diff --git a/aurora-main/lib/gfx/cockpit.hpp b/aurora-main/lib/gfx/cockpit.hpp index ba81b39..7b8aaf2 100644 --- a/aurora-main/lib/gfx/cockpit.hpp +++ b/aurora-main/lib/gfx/cockpit.hpp @@ -92,25 +92,36 @@ inline void ellipsoid(std::vector& vertices,V center,V radii,V color) { if(ring<5) triangle(vertices,b,c,d,color); } } +// Rounded palm and individually articulated fingers, in the controller's grip +// space as OpenXR defines it: the origin is the palm centroid, -Z runs up the +// tube the curled fingers form (little finger towards thumb), and +X is the +// palm's outward normal, so the fingers close towards +X. +Y completes the +// right-handed frame, which leaves it along the fingers on the right hand and +// against them on the left. Building the fingers on any other axis bends them +// out of the back of the hand (seen on a Quest 3 on 2026-09-22). inline void glove(std::vector& v, const AuroraCockpitHand& hand, int side) { const size_t start=v.size(); const V white{0.91f,0.95f,1.0f}; - // Rounded palm and individually articulated fingers in grip-local metres. - ellipsoid(v,{0,0,0},{0.041f,0.018f,0.043f},white); + const float forward=side==0?-1.0f:1.0f; // hand 0 is the left one const float curl=std::clamp(hand.held?0.85f:hand.squeeze,0.0f,1.0f); + // Thin through the palm's normal, a little wider across the knuckles than + // the palm is long. + ellipsoid(v,{0,0,0},{0.018f,0.043f,0.041f},white); for(int finger=0;finger<4;++finger) { - V a{-0.025f+finger*0.017f,0,-0.028f}; + // Index finger nearest the thumb (-Z), little finger last. + V a{0.0f,forward*0.030f,-0.025f+finger*0.017f}; const float length=finger==0||finger==3?0.021f:0.026f; for(int joint=0;joint<3;++joint) { const float angle=curl*(0.55f+joint*0.8f); - V b=add(a,{0,-std::sin(angle)*length,-std::cos(angle)*length}); + V b=add(a,{std::sin(angle)*length,forward*std::cos(angle)*length,0.0f}); tube(v,a,b,0.008f,white); ellipsoid(v,b,{0.008f,0.008f,0.008f},white);a=b; } } - const float sign=side? -1.0f:1.0f; - tube(v,{sign*0.025f,0,0.012f},{sign*0.048f,-0.012f,-0.012f},0.010f,white); - tube(v,{sign*0.048f,-0.012f,-0.012f},{sign*(0.055f-0.021f*curl),-0.018f,-0.041f},0.009f,white); + // Thumb: out of the palm's thumb side, closing across the fingers. + const V thumbKnuckle{0.026f,forward*0.034f,-0.030f}; + tube(v,{0.010f,forward*0.012f,-0.034f},thumbKnuckle,0.010f,white); + tube(v,thumbKnuckle,{0.030f+0.014f*curl,forward*(0.052f-0.016f*curl),-0.020f},0.009f,white); for(size_t i=start;i& out, const AuroraCockpitHand& hand, const HandMesh& mesh) { diff --git a/aurora-main/tests/cockpit_geometry_test.cpp b/aurora-main/tests/cockpit_geometry_test.cpp index 94f1ad7..144b822 100644 --- a/aurora-main/tests/cockpit_geometry_test.cpp +++ b/aurora-main/tests/cockpit_geometry_test.cpp @@ -112,6 +112,47 @@ TEST_F(CockpitGeometry, TrackedHandDrawsAGloveAtItsGrip) { } } +// The grip space OpenXR defines: -Z up the curled fingers' tube towards the +// thumb, +X out of the palm. So the fingers run along Y (+Y on the right hand, +// -Y on the left) and close towards +X, never out of the back of the hand. +TEST_F(CockpitGeometry, GloveFingersRunAlongTheHandAndCloseIntoThePalm) { + for (int side = 0; side < 2; ++side) { + const float forward = side == 0 ? -1.0f : 1.0f; + const auto build = [&](float squeeze) { + AuroraCockpit cockpit{}; + cockpit.nativeWheel = true; + cockpit.hands[side].tracked = true; + cockpit.hands[side].squeeze = squeeze; + set_identity(cockpit.hands[side].seatFromGrip, {0.0f, 0.0f, 0.0f}); + return aurora::gfx::cockpit::geometry(cockpit); + }; + struct Extent { + float reach = 0.0f; // furthest along the fingers + float palm = 0.0f; // furthest towards the palm's normal + float back = 0.0f; // furthest out of the back of the hand + float across = 0.0f; // furthest across the knuckles + }; + const auto measure = [&](const std::vector& vertices) { + Extent e{}; + for (const auto& vertex : vertices) { + e.reach = std::max(e.reach, vertex.position[1] * forward); + e.palm = std::max(e.palm, vertex.position[0]); + e.back = std::min(e.back, vertex.position[0]); + e.across = std::max(e.across, std::abs(vertex.position[2])); + } + return e; + }; + const auto open = measure(build(0.0f)); + const auto closed = measure(build(1.0f)); + EXPECT_GT(open.reach, 0.09f) << "open fingers reach along the hand, side " << side; + EXPECT_LT(open.palm, 0.05f) << "an open hand is flat, side " << side; + EXPECT_LT(closed.reach, open.reach - 0.02f) << "closing shortens the reach, side " << side; + EXPECT_GT(closed.palm, open.palm + 0.02f) << "closing moves the fingers into the palm, side " << side; + EXPECT_GT(closed.back, -0.03f) << "fingers never bend out of the back of the hand, side " << side; + EXPECT_LT(closed.across, 0.07f) << "fingers stay across the knuckles, side " << side; + } +} + TEST_F(CockpitGeometry, RuntimeHandMeshIsSkinnedWithoutNans) { using namespace aurora::gfx::cockpit; auto mesh = std::make_shared();