mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 03:00:14 +02:00
Centre foveation on the player's gaze on headsets with eye tracking
With [vr] eye_tracked_foveation (on by default on the Steam Frame, off elsewhere) the runtime asks for XR_EXT_eye_gaze_interaction. When the system reports an eye tracker, OpenXRInput binds the gaze pose and locates it for each packet's display time, in the space the eye views are located in; vr/eye_gaze.h turns it into tangents of each eye's own view, which AuroraStereoFrame now carries (appended, after the existing prefix). Aurora centres the eye's fragment density map on the gaze snapped to a cell of two map texels (about 3 degrees). Each eye keeps up to 32 maps, one per cell looked at, so a glance back reuses its map; a new map is bound once its upload completes, and until then the eye keeps the map it had. Without a tracked gaze (a blink, no tracker, the setting off) foveation centres on the forward direction exactly as before: the forward maps are byte-identical. Also logs every extension the OpenXR runtime offers at startup, so the first Steam Frame session shows what SteamVR's Android runtime has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
This commit is contained in:
16 files changed
+616
-47
No files matched your search
@@ -175,5 +175,112 @@ TEST(Foveation, ReadsTheFieldOfViewBackFromTheEyeProjection) {
|
||||
EXPECT_EQ(fallback.tanUp, 1.0f);
|
||||
}
|
||||
|
||||
// Eye-tracked foveation: the full-density centre follows the gaze.
|
||||
|
||||
// The pixel a gaze lands on, as build lays the eye out.
|
||||
std::pair<float, float> gaze_pixel(const EyeFov& fov, const Gaze& gaze, uint32_t width, uint32_t height) {
|
||||
return {(gaze.tanX - fov.tanLeft) / (fov.tanRight - fov.tanLeft) * width,
|
||||
(gaze.tanY - fov.tanUp) / (fov.tanDown - fov.tanUp) * height};
|
||||
}
|
||||
|
||||
TEST(Foveation, TheForwardGazeKeepsTheFixedMap) {
|
||||
for (Level level : {Level::Low, Level::Medium, Level::High}) {
|
||||
Map gazed;
|
||||
foveation::build(1344, 1408, 32, left_eye(), level, gazed, Gaze{});
|
||||
EXPECT_TRUE(gazed.rg8 == build_map(level).rg8) << "level " << int(level);
|
||||
}
|
||||
// The general angle agrees with the forward one.
|
||||
for (float tanX : {-1.2f, -0.3f, 0.0f, 0.4f, 0.9f}) {
|
||||
for (float tanY : {-1.0f, 0.0f, 0.7f}) {
|
||||
EXPECT_NEAR(angle_from_gaze_degrees(tanX, tanY, Gaze{}), eccentricity_degrees(tanX, tanY), 0.01f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Foveation, TheFullDensityRegionFollowsTheGaze) {
|
||||
const EyeFov fov = left_eye();
|
||||
// Down and to the right, well off the forward direction.
|
||||
const Gaze gaze{.tanX = std::tan(20.0f * kDegrees), .tanY = std::tan(-15.0f * kDegrees)};
|
||||
Map map;
|
||||
foveation::build(1344, 1408, 32, fov, Level::High, map, gaze);
|
||||
double sumX = 0.0;
|
||||
double sumY = 0.0;
|
||||
uint32_t count = 0;
|
||||
for (uint32_t y = 0; y < map.height; ++y) {
|
||||
for (uint32_t x = 0; x < map.width; ++x) {
|
||||
if (at(map, x, y) == kFullDensity) {
|
||||
sumX += x + 0.5;
|
||||
sumY += y + 0.5;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_GT(count, 0u);
|
||||
const auto [pixelX, pixelY] = gaze_pixel(fov, gaze, 1344, 1408);
|
||||
EXPECT_NEAR(sumX / count, pixelX / 32.0, 1.5);
|
||||
EXPECT_NEAR(sumY / count, pixelY / 32.0, 1.5);
|
||||
// Where the forward map was sharpest, the far side of the gaze is now coarse.
|
||||
const Map fixed = build_map(Level::High, fov);
|
||||
EXPECT_EQ(at(map, static_cast<uint32_t>(pixelX / 32.0f), static_cast<uint32_t>(pixelY / 32.0f)), kFullDensity);
|
||||
EXPECT_EQ(at(fixed, 0, 0), kQuarterDensity);
|
||||
EXPECT_EQ(at(map, 0, 0), kQuarterDensity);
|
||||
}
|
||||
|
||||
TEST(Foveation, DensityNeverRisesAwayFromTheGaze) {
|
||||
const EyeFov fov = left_eye();
|
||||
const Gaze gaze{.tanX = -0.35f, .tanY = 0.2f};
|
||||
for (Level level : {Level::Low, Level::Medium, Level::High}) {
|
||||
Map map;
|
||||
foveation::build(1344, 1408, 32, fov, level, map, gaze);
|
||||
std::vector<std::pair<float, uint8_t>> texels;
|
||||
for (uint32_t y = 0; y < map.height; ++y) {
|
||||
for (uint32_t x = 0; x < map.width; ++x) {
|
||||
const auto [tanX, tanY] = tangents(map, fov, x, y, 1344, 1408, 32);
|
||||
texels.emplace_back(angle_from_gaze_degrees(tanX, tanY, gaze), at(map, x, y));
|
||||
}
|
||||
}
|
||||
std::sort(texels.begin(), texels.end());
|
||||
for (size_t i = 1; i < texels.size(); ++i) {
|
||||
EXPECT_LE(texels[i].second, texels[i - 1].second);
|
||||
}
|
||||
EXPECT_EQ(texels.front().second, kFullDensity);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Foveation, GazeCellsSnapTheGazeAndStayInsideTheEye) {
|
||||
const EyeFov fov = left_eye();
|
||||
constexpr uint32_t kWidth = 1344, kHeight = 1408, kTexel = 32;
|
||||
constexpr float kCellPixels = kTexel * kGazeCellTexels;
|
||||
// A cell's own gaze lies within half a cell of every gaze that falls in it.
|
||||
for (float tanX : {-0.9f, -0.2f, 0.0f, 0.31f, 0.8f}) {
|
||||
for (float tanY : {-0.8f, 0.0f, 0.45f}) {
|
||||
const Gaze gaze{.tanX = tanX, .tanY = tanY};
|
||||
const GazeCell cell = gaze_cell(kWidth, kHeight, kTexel, fov, gaze);
|
||||
const Gaze centre = cell_gaze(kWidth, kHeight, kTexel, fov, cell);
|
||||
const auto [gx, gy] = gaze_pixel(fov, gaze, kWidth, kHeight);
|
||||
const auto [cx, cy] = gaze_pixel(fov, centre, kWidth, kHeight);
|
||||
EXPECT_LE(std::abs(gx - cx), kCellPixels / 2.0f + 0.01f);
|
||||
EXPECT_LE(std::abs(gy - cy), kCellPixels / 2.0f + 0.01f);
|
||||
EXPECT_TRUE(gaze_cell(kWidth, kHeight, kTexel, fov, centre) == cell);
|
||||
}
|
||||
}
|
||||
// Gazes a few pixels apart share a cell; the forward direction has one of its own.
|
||||
const Gaze forward{};
|
||||
const GazeCell forwardCell = gaze_cell(kWidth, kHeight, kTexel, fov, forward);
|
||||
const auto [fx, fy] = gaze_pixel(fov, forward, kWidth, kHeight);
|
||||
EXPECT_EQ(forwardCell.x, static_cast<int32_t>(fx / kCellPixels));
|
||||
EXPECT_EQ(forwardCell.y, static_cast<int32_t>(fy / kCellPixels));
|
||||
// Beyond the eye, and not a number at all.
|
||||
const int32_t lastColumn = static_cast<int32_t>(std::ceil(kWidth / kCellPixels)) - 1;
|
||||
const int32_t lastRow = static_cast<int32_t>(std::ceil(kHeight / kCellPixels)) - 1;
|
||||
const GazeCell far = gaze_cell(kWidth, kHeight, kTexel, fov, Gaze{.tanX = 10.0f, .tanY = -10.0f});
|
||||
EXPECT_EQ(far.x, lastColumn);
|
||||
EXPECT_EQ(far.y, lastRow);
|
||||
const GazeCell farOther = gaze_cell(kWidth, kHeight, kTexel, fov, Gaze{.tanX = -10.0f, .tanY = 10.0f});
|
||||
EXPECT_EQ(farOther.x, 0);
|
||||
EXPECT_EQ(farOther.y, 0);
|
||||
EXPECT_TRUE(gaze_cell(kWidth, kHeight, kTexel, fov, Gaze{.tanX = NAN, .tanY = 0.2f}) == forwardCell);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace aurora::gfx::foveation
|
||||
Reference in new issue
Block a user