mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 05:00:27 +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
@@ -456,6 +456,11 @@ add_executable(mkw_vr_wii_remote_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_wii_r
|
||||
target_include_directories(mkw_vr_wii_remote_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_vr_wii_remote_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_wii_remote_tests COMMAND mkw_vr_wii_remote_tests)
|
||||
# Eye-tracked foveation: the gaze in each eye's view (vr/eye_gaze.h).
|
||||
add_executable(mkw_vr_eye_gaze_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_eye_gaze_tests.cpp")
|
||||
target_include_directories(mkw_vr_eye_gaze_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_vr_eye_gaze_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_eye_gaze_tests COMMAND mkw_vr_eye_gaze_tests)
|
||||
|
||||
# The in-headset settings panel's controller chord, release latch, selection,
|
||||
# scrolling and canvas mapping, plus the thread bridge they publish through.
|
||||
|
||||
@@ -92,6 +92,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> vrWheelHaptics;
|
||||
std::optional<std::string> vrPerformanceLevel;
|
||||
std::optional<std::string> vrFoveation;
|
||||
std::optional<bool> vrEyeTrackedFoveation;
|
||||
std::optional<std::string> vrRecenterKey;
|
||||
std::optional<float> vrLeanBackDegrees;
|
||||
// F10 > Diagnostics: OpenXR pacing and presentation logging in console.log.
|
||||
@@ -320,6 +321,16 @@ inline bool IsSupportedVrFoveation(std::string_view value) {
|
||||
return std::find(kVrFoveationLevels.begin(), kVrFoveationLevels.end(), value) != kVrFoveationLevels.end();
|
||||
}
|
||||
|
||||
// Eye-tracked foveation: with a headset that tracks the eyes (XR_EXT_eye_gaze_interaction, the Steam
|
||||
// Frame's), the foveation level's full-density region follows the gaze instead of staying on each
|
||||
// eye's forward direction. Live while the session's runtime offered the gaze at launch. On by
|
||||
// default on the Steam Frame; elsewhere off, since Horizon OS asks for an eye tracking permission.
|
||||
#if defined(MKW_HEADSET_STEAM_FRAME)
|
||||
inline constexpr bool kVrEyeTrackedFoveationDefault = true;
|
||||
#else
|
||||
inline constexpr bool kVrEyeTrackedFoveationDefault = false;
|
||||
#endif
|
||||
|
||||
// The level aurora_set_stereo_foveation takes; anything unknown is off.
|
||||
inline uint32_t VrFoveationLevelIndex(std::string_view value) {
|
||||
const auto it = std::find(kVrFoveationLevels.begin(), kVrFoveationLevels.end(), value);
|
||||
@@ -891,6 +902,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
value && IsSupportedVrFoveation(*value)) {
|
||||
config.vrFoveation = *value;
|
||||
}
|
||||
config.vrEyeTrackedFoveation = FindConfigValue<bool>(document, "vr", "eye_tracked_foveation");
|
||||
if (auto value = FindConfigValue<std::string>(document, "vr", "mirror_view");
|
||||
value && IsSupportedVrMirrorView(*value)) {
|
||||
config.vrMirrorView = *value;
|
||||
@@ -1303,6 +1315,11 @@ inline bool SetVrFoveation(std::string value) {
|
||||
return WriteSetting("vr", "foveation", FormatString(value));
|
||||
}
|
||||
|
||||
inline bool SetVrEyeTrackedFoveation(bool value) {
|
||||
Mutable().vrEyeTrackedFoveation = value;
|
||||
return WriteSetting("vr", "eye_tracked_foveation", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonSeat(std::string value) {
|
||||
if (!IsSupportedVrFirstPersonSeat(value)) {
|
||||
return false;
|
||||
@@ -1828,6 +1845,10 @@ inline std::string VrFoveation(std::string fallback = kVrFoveationDefault) {
|
||||
return value && IsSupportedVrFoveation(*value) ? *value : std::move(fallback);
|
||||
}
|
||||
|
||||
inline bool VrEyeTrackedFoveation(bool fallback = kVrEyeTrackedFoveationDefault) {
|
||||
return Get().vrEyeTrackedFoveation.value_or(fallback);
|
||||
}
|
||||
|
||||
inline int32_t VrFirstPersonHiddenModel(int32_t fallback = kVrFirstPersonHiddenModelDefault) {
|
||||
return std::clamp(Get().vrFirstPersonHiddenModel.value_or(fallback), -1, 31);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
// Eye-tracked foveation ([vr] eye_tracked_foveation): where the player looks, as each eye's image
|
||||
// measures it. Kept free of OpenXR types so it can be checked headlessly (tests/vr_eye_gaze_tests.cpp).
|
||||
//
|
||||
// Conventions are OpenXR's: right-handed, +Y up, and a pose looks down its -Z axis.
|
||||
namespace mkw::vr::eye_gaze {
|
||||
|
||||
struct Quaternion {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
float w = 1.0f;
|
||||
};
|
||||
|
||||
// Tangents of an eye's view, x right and y up, as its frustum (XrFovf) measures them.
|
||||
struct Tangents {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
// Beyond this angle from an eye's forward direction a gaze is no point of its image (cos 80 deg).
|
||||
inline constexpr float kMinForwardCosine = 0.17364818f;
|
||||
|
||||
// The gaze pose's look direction in one eye's view, from both orientations in the same space. The
|
||||
// eyes' views can be canted outwards, so each eye gets its own tangents.
|
||||
inline Tangents InEye(Quaternion gaze, Quaternion eye) noexcept {
|
||||
const auto normalized = [](Quaternion q) {
|
||||
const float length = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
|
||||
if (!(length > 1.0e-6f)) {
|
||||
return Quaternion{};
|
||||
}
|
||||
return Quaternion{q.x / length, q.y / length, q.z / length, q.w / length};
|
||||
};
|
||||
// q * v * conjugate(q).
|
||||
const auto rotate = [](const Quaternion& q, float vx, float vy, float vz, float out[3]) {
|
||||
const float tx = 2.0f * (q.y * vz - q.z * vy);
|
||||
const float ty = 2.0f * (q.z * vx - q.x * vz);
|
||||
const float tz = 2.0f * (q.x * vy - q.y * vx);
|
||||
out[0] = vx + q.w * tx + (q.y * tz - q.z * ty);
|
||||
out[1] = vy + q.w * ty + (q.z * tx - q.x * tz);
|
||||
out[2] = vz + q.w * tz + (q.x * ty - q.y * tx);
|
||||
};
|
||||
gaze = normalized(gaze);
|
||||
eye = normalized(eye);
|
||||
float look[3];
|
||||
rotate(gaze, 0.0f, 0.0f, -1.0f, look);
|
||||
float seen[3];
|
||||
rotate(Quaternion{-eye.x, -eye.y, -eye.z, eye.w}, look[0], look[1], look[2], seen);
|
||||
if (!(-seen[2] > kMinForwardCosine)) {
|
||||
return {};
|
||||
}
|
||||
return {seen[0] / -seen[2], seen[1] / -seen[2], true};
|
||||
}
|
||||
|
||||
} // namespace mkw::vr::eye_gaze
|
||||
@@ -130,6 +130,15 @@ public:
|
||||
// The tracked hands' joints in the seated frame, as the last Sync located
|
||||
// them; read on the pacing thread only.
|
||||
const hand_tracking::HandJointFrame& HandJoints() const noexcept { return m_joint_frame; }
|
||||
// Eye-tracked foveation: the gaze pose's orientation in the app space, as the last Sync located
|
||||
// it for the frame's display time. False while there is no tracked gaze (no eye tracker, a
|
||||
// blink, or the runtime not offering XR_EXT_eye_gaze_interaction).
|
||||
bool EyeGaze(XrQuaternionf* orientation) const noexcept {
|
||||
if (m_gaze_valid && orientation != nullptr) {
|
||||
*orientation = m_gaze_orientation;
|
||||
}
|
||||
return m_gaze_valid;
|
||||
}
|
||||
// A hand's tracker while tracked hands keep one, else XR_NULL_HANDLE.
|
||||
XrHandTrackerEXT HandTracker(uint32_t hand) const noexcept {
|
||||
return hand < kHands ? m_hand_trackers[hand] : XR_NULL_HANDLE;
|
||||
@@ -150,6 +159,9 @@ private:
|
||||
|
||||
bool CreateActions();
|
||||
bool SuggestBindings();
|
||||
// XR_EXT_eye_gaze_interaction is enabled and the system has an eye tracker.
|
||||
bool EyeGazeOffered();
|
||||
void LocateEyeGaze(XrTime time);
|
||||
void CreatePoseSpaces();
|
||||
void DestroyPoseSpaces();
|
||||
void LoadInputClock();
|
||||
@@ -201,6 +213,12 @@ private:
|
||||
XrAction m_aim_pose = XR_NULL_HANDLE;
|
||||
XrAction m_grip_pose = XR_NULL_HANDLE;
|
||||
XrAction m_haptic = XR_NULL_HANDLE;
|
||||
// The eyes' gaze (XR_EXT_eye_gaze_interaction), one pose for both, and where it was last located.
|
||||
XrAction m_gaze_pose = XR_NULL_HANDLE;
|
||||
XrSpace m_gaze_space = XR_NULL_HANDLE;
|
||||
XrQuaternionf m_gaze_orientation{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
bool m_gaze_valid = false;
|
||||
bool m_gaze_logged = false;
|
||||
XrPath m_hand_paths[kHands]{};
|
||||
XrSpace m_aim_spaces[kHands]{};
|
||||
XrSpace m_grip_spaces[kHands]{};
|
||||
|
||||
@@ -156,6 +156,7 @@ bool g_vrHandTracking = RuntimeConfigFile::VrHandTracking();
|
||||
constexpr std::array<const char*, 4> kVrFoveationLabels{"Off", "Low", "Medium", "High"};
|
||||
static_assert(kVrFoveationLabels.size() == RuntimeConfigFile::kVrFoveationLevels.size());
|
||||
int g_vrFoveation = static_cast<int>(RuntimeConfigFile::VrFoveationLevelIndex(RuntimeConfigFile::VrFoveation()));
|
||||
bool g_vrEyeTrackedFoveation = RuntimeConfigFile::VrEyeTrackedFoveation();
|
||||
#endif
|
||||
bool g_vrFirstPerson = RuntimeConfigFile::VrFirstPerson(false);
|
||||
bool g_vrFirstPersonToggleClick = RuntimeConfigFile::VrFirstPersonToggleClick();
|
||||
@@ -1523,6 +1524,15 @@ void DrawVrSettings() {
|
||||
"lenses blur the picture anyway, to free GPU time. This session started with it "
|
||||
"off, or without a GPU that supports it: a new level applies after a restart.");
|
||||
}
|
||||
if (ImGui::Checkbox("Foveation follows the eyes", &g_vrEyeTrackedFoveation)) {
|
||||
RuntimeConfigFile::SetVrEyeTrackedFoveation(g_vrEyeTrackedFoveation);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"With a headset that tracks the eyes (the Steam Frame), the sharp centre of the foveated "
|
||||
"race view moves to where you look instead of staying straight ahead. Turning it off "
|
||||
"applies immediately; turning it on needs a restart if the session started without it.");
|
||||
}
|
||||
#endif
|
||||
{
|
||||
const auto rateLabel = [](uint32_t hz) {
|
||||
|
||||
@@ -423,6 +423,36 @@ bool OpenXRInput::CreateActions() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Eye-tracked foveation: one gaze pose for both eyes, with no hand to name.
|
||||
if (EyeGazeOffered()) {
|
||||
XrActionCreateInfo info{XR_TYPE_ACTION_CREATE_INFO};
|
||||
info.actionType = XR_ACTION_TYPE_POSE_INPUT;
|
||||
std::strncpy(info.actionName, "eye_gaze", XR_MAX_ACTION_NAME_SIZE - 1);
|
||||
std::strncpy(info.localizedActionName, "Eye Gaze", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
||||
if (!Check(xrCreateAction(m_action_set, &info, &m_gaze_pose), "eye_gaze")) {
|
||||
// Not worth the controllers: carry on with the fixed foveation centre.
|
||||
m_gaze_pose = XR_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenXRInput::EyeGazeOffered() {
|
||||
const auto& extensions = m_runtime->EnabledExtensions();
|
||||
if (std::find(extensions.begin(), extensions.end(), "XR_EXT_eye_gaze_interaction") == extensions.end()) {
|
||||
return false;
|
||||
}
|
||||
XrSystemEyeGazeInteractionPropertiesEXT gaze{XR_TYPE_SYSTEM_EYE_GAZE_INTERACTION_PROPERTIES_EXT};
|
||||
XrSystemProperties properties{XR_TYPE_SYSTEM_PROPERTIES, &gaze};
|
||||
const XrResult result = xrGetSystemProperties(m_runtime->Instance(), m_runtime->SystemId(), &properties);
|
||||
if (XR_FAILED(result) || gaze.supportsEyeGazeInteraction != XR_TRUE) {
|
||||
std::ostringstream message;
|
||||
message << "OpenXR eye gaze: the runtime reports no eye tracker (" << result
|
||||
<< "); foveation stays on each eye's forward direction";
|
||||
Log(OpenXRLogLevel::Info, message.str());
|
||||
return false;
|
||||
}
|
||||
Log(OpenXRLogLevel::Info, "OpenXR eye gaze: available; foveation follows the gaze ([vr] eye_tracked_foveation)");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -533,6 +563,10 @@ bool OpenXRInput::SuggestBindings() {
|
||||
};
|
||||
suggest("/interaction_profiles/valve/frame_controller_valve", frame, false);
|
||||
}
|
||||
if (m_gaze_pose != XR_NULL_HANDLE) {
|
||||
suggest("/interaction_profiles/ext/eye_gaze_interaction",
|
||||
{{&m_gaze_pose, "/user/eyes_ext/input/gaze_ext/pose"}}, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -558,9 +592,56 @@ void OpenXRInput::CreatePoseSpaces() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_gaze_pose != XR_NULL_HANDLE) {
|
||||
XrActionSpaceCreateInfo info{XR_TYPE_ACTION_SPACE_CREATE_INFO};
|
||||
info.action = m_gaze_pose;
|
||||
info.poseInActionSpace.orientation.w = 1.0f;
|
||||
const XrResult result = xrCreateActionSpace(m_runtime->Session(), &info, &m_gaze_space);
|
||||
m_runtime->ObserveResult(result);
|
||||
if (XR_FAILED(result)) {
|
||||
m_gaze_space = XR_NULL_HANDLE;
|
||||
std::ostringstream message;
|
||||
message << "xrCreateActionSpace(eye gaze) failed (" << result
|
||||
<< "); foveation stays on each eye's forward direction";
|
||||
Log(OpenXRLogLevel::Warning, message.str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eye-tracked foveation: the gaze for the display time the eyes are rendered for, the time their
|
||||
// views are located at, so the full-density region lands where the eyes look in that frame. Only a
|
||||
// tracked orientation counts; the runtime reports an untracked one through blinks.
|
||||
void OpenXRInput::LocateEyeGaze(XrTime time) {
|
||||
m_gaze_valid = false;
|
||||
if (m_gaze_space == XR_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
XrActionStateGetInfo info{XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
info.action = m_gaze_pose;
|
||||
XrActionStatePose state{XR_TYPE_ACTION_STATE_POSE};
|
||||
if (XR_FAILED(xrGetActionStatePose(m_runtime->Session(), &info, &state)) || state.isActive != XR_TRUE) {
|
||||
return;
|
||||
}
|
||||
constexpr XrSpaceLocationFlags kTracked =
|
||||
XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT;
|
||||
XrSpaceLocation location{XR_TYPE_SPACE_LOCATION};
|
||||
if (XR_FAILED(xrLocateSpace(m_gaze_space, m_runtime->AppSpace(), time, &location)) ||
|
||||
(location.locationFlags & kTracked) != kTracked) {
|
||||
return;
|
||||
}
|
||||
m_gaze_orientation = location.pose.orientation;
|
||||
m_gaze_valid = true;
|
||||
if (!m_gaze_logged) {
|
||||
m_gaze_logged = true;
|
||||
Log(OpenXRLogLevel::Info, "OpenXR eye gaze: tracking");
|
||||
}
|
||||
}
|
||||
|
||||
void OpenXRInput::DestroyPoseSpaces() {
|
||||
if (m_gaze_space != XR_NULL_HANDLE) {
|
||||
xrDestroySpace(m_gaze_space);
|
||||
m_gaze_space = XR_NULL_HANDLE;
|
||||
}
|
||||
for (uint32_t hand = 0; hand < kHandCount; ++hand) {
|
||||
for (XrSpace* space : {&m_aim_spaces[hand], &m_grip_spaces[hand]}) {
|
||||
if (*space != XR_NULL_HANDLE) {
|
||||
@@ -909,6 +990,9 @@ void OpenXRInput::Destroy() {
|
||||
m_thumbstick = m_thumbstick_click = m_trigger = m_squeeze = XR_NULL_HANDLE;
|
||||
m_button_primary = m_button_secondary = m_menu = m_haptic = XR_NULL_HANDLE;
|
||||
m_dpad_up = m_dpad_down = m_dpad_left = m_dpad_right = XR_NULL_HANDLE;
|
||||
m_gaze_pose = XR_NULL_HANDLE;
|
||||
m_gaze_valid = false;
|
||||
m_gaze_logged = false;
|
||||
m_aim_pose = m_grip_pose = XR_NULL_HANDLE;
|
||||
m_hand_paths[0] = m_hand_paths[1] = XR_NULL_PATH;
|
||||
m_convert_now_to_xr_time = nullptr;
|
||||
@@ -933,6 +1017,7 @@ void OpenXRInput::Idle() {
|
||||
}
|
||||
m_pointer.Reset();
|
||||
m_horizon = {1.0f, 0.0f};
|
||||
m_gaze_valid = false;
|
||||
OpenXRPublishWiiRemote(Relay().JoystickId(), OpenXRWiiRemoteSample{});
|
||||
// The panel stays as it was; only what the controllers were holding is forgotten.
|
||||
m_panel_controls.Reset();
|
||||
@@ -976,6 +1061,7 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
|
||||
Idle();
|
||||
return;
|
||||
}
|
||||
LocateEyeGaze(predicted_display_time);
|
||||
|
||||
// `active`, when given, says whether the action is bound to a source the
|
||||
// runtime has right now (a controller, or a tracked hand).
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "runtime_config.h"
|
||||
#include "gx_thread.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/eye_gaze.h"
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
@@ -428,6 +429,10 @@ public:
|
||||
// Horizon OS's room view; the Steam Frame build neither asks for it nor offers the setting.
|
||||
config.optional_extensions.push_back("XR_FB_passthrough");
|
||||
#endif
|
||||
// Eye-tracked foveation: the gaze the density maps centre on (OpenXRInput::EyeGaze).
|
||||
if (RuntimeConfigFile::VrEyeTrackedFoveation()) {
|
||||
config.optional_extensions.push_back("XR_EXT_eye_gaze_interaction");
|
||||
}
|
||||
AddHandMeshExtensions(config);
|
||||
config.instance_create_next = OpenXRAndroidInstanceCreateNext();
|
||||
#endif
|
||||
@@ -1465,9 +1470,30 @@ private:
|
||||
position_valid && base_position_valid_, units_per_meter,
|
||||
lean_back_radians, destination.eyes[eye].viewFromCenter);
|
||||
}
|
||||
BuildEyeGaze(source, destination);
|
||||
BuildCockpit(source, position_valid, units_per_meter, lean_back_radians, destination.cockpit);
|
||||
}
|
||||
|
||||
// Eye-tracked foveation: where the eyes look, in each eye's own view (the views may be canted),
|
||||
// from the gaze the input located for this packet's display time. Without a tracked gaze, or
|
||||
// with the setting off, Aurora centres foveation on each eye's forward direction.
|
||||
void BuildEyeGaze(const OpenXRBackendFrame& source, AuroraStereoFrame& destination) const noexcept {
|
||||
XrQuaternionf gaze{};
|
||||
if (input_ == nullptr || !RuntimeConfigFile::VrEyeTrackedFoveation() || !input_->EyeGaze(&gaze)) {
|
||||
return;
|
||||
}
|
||||
bool valid = true;
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
const XrQuaternionf& view = source.xr_frame.views[eye].pose.orientation;
|
||||
const eye_gaze::Tangents seen =
|
||||
eye_gaze::InEye({gaze.x, gaze.y, gaze.z, gaze.w}, {view.x, view.y, view.z, view.w});
|
||||
valid = valid && seen.valid;
|
||||
destination.gaze[eye][0] = seen.x;
|
||||
destination.gaze[eye][1] = seen.y;
|
||||
}
|
||||
destination.gazeValid = valid;
|
||||
}
|
||||
|
||||
// The first-person cockpit's hands and separate wheel, in the seated frame
|
||||
// the eye transforms place at base + lean * seat (metres). Always carries
|
||||
// the packet's world scale, which Aurora rescales to the sealed frame's.
|
||||
|
||||
@@ -151,6 +151,14 @@ bool OpenXRRuntime::EnumerateInstanceCapabilities() {
|
||||
for (const XrExtensionProperties& extension : extension_properties) {
|
||||
m_available_extensions.emplace_back(extension.extensionName);
|
||||
}
|
||||
// Everything the runtime offers, once per instance: what a new headset's runtime can do (the
|
||||
// Steam Frame's SteamVR, say) is in the first session log rather than behind a debugger.
|
||||
std::ostringstream offered;
|
||||
offered << "OpenXR runtime offers " << m_available_extensions.size() << " extensions:";
|
||||
for (const std::string& name : m_available_extensions) {
|
||||
offered << ' ' << name;
|
||||
}
|
||||
Log(OpenXRLogLevel::Info, offered.str());
|
||||
|
||||
uint32_t layer_count = 0;
|
||||
if (!Check(xrEnumerateApiLayerProperties(0, &layer_count, nullptr),
|
||||
|
||||
@@ -68,6 +68,17 @@ int main() {
|
||||
Require(std::string_view(MKW_VR_REFRESH_RATE_DEFAULT_TEXT) == "0");
|
||||
#endif
|
||||
|
||||
// [vr] eye_tracked_foveation: the foveation centre follows the gaze, on the Steam Frame by default.
|
||||
Require(Parse("[vr]\neye_tracked_foveation = true\n").vrEyeTrackedFoveation == true);
|
||||
Require(Parse("[vr]\neye_tracked_foveation = false\n").vrEyeTrackedFoveation == false);
|
||||
Require(!Parse("[vr]\neye_tracked_foveation = 1\n").vrEyeTrackedFoveation.has_value());
|
||||
Require(!Parse("[vr]\n").vrEyeTrackedFoveation.has_value());
|
||||
#if defined(MKW_HEADSET_STEAM_FRAME)
|
||||
Require(RuntimeConfigFile::kVrEyeTrackedFoveationDefault);
|
||||
#else
|
||||
Require(!RuntimeConfigFile::kVrEyeTrackedFoveationDefault);
|
||||
#endif
|
||||
|
||||
// [vr] passthrough: Horizon OS's room view, which the Steam Frame build does not offer.
|
||||
Require(Parse("[vr]\npassthrough = false\n").vrPassthrough == false);
|
||||
Require(!Parse("[vr]\n").vrPassthrough.has_value());
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "vr/eye_gaze.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
using mkw::vr::eye_gaze::InEye;
|
||||
using mkw::vr::eye_gaze::Quaternion;
|
||||
using mkw::vr::eye_gaze::Tangents;
|
||||
|
||||
namespace {
|
||||
|
||||
void Check(bool condition, const char* what) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "vr_eye_gaze_tests: %s\n", what);
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckNear(float value, float expected, const char* what) {
|
||||
Check(std::fabs(value - expected) <= 1.0e-4f, what);
|
||||
}
|
||||
|
||||
constexpr float kDegrees = 3.14159265358979f / 180.0f;
|
||||
|
||||
// A turn by `degrees` about the unit axis (x, y, z).
|
||||
Quaternion Turn(float degrees, float x, float y, float z) {
|
||||
const float half = 0.5f * degrees * kDegrees;
|
||||
return {x * std::sin(half), y * std::sin(half), z * std::sin(half), std::cos(half)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
// Looking straight ahead through an eye looking straight ahead: its forward direction.
|
||||
Tangents t = InEye({}, {});
|
||||
Check(t.valid, "straight ahead is valid");
|
||||
CheckNear(t.x, 0.0f, "straight ahead: x");
|
||||
CheckNear(t.y, 0.0f, "straight ahead: y");
|
||||
|
||||
// A turn about +Y by a positive angle looks left (-X), about +X looks up (+Y).
|
||||
t = InEye(Turn(20.0f, 0.0f, 1.0f, 0.0f), {});
|
||||
Check(t.valid, "20 degrees left is valid");
|
||||
CheckNear(t.x, -std::tan(20.0f * kDegrees), "20 degrees left: x");
|
||||
CheckNear(t.y, 0.0f, "20 degrees left: y");
|
||||
t = InEye(Turn(15.0f, 1.0f, 0.0f, 0.0f), {});
|
||||
CheckNear(t.x, 0.0f, "15 degrees up: x");
|
||||
CheckNear(t.y, std::tan(15.0f * kDegrees), "15 degrees up: y");
|
||||
|
||||
// An eye canted outwards sees the same gaze off its own centre.
|
||||
t = InEye({}, Turn(10.0f, 0.0f, 1.0f, 0.0f));
|
||||
CheckNear(t.x, std::tan(10.0f * kDegrees), "a left-canted eye sees straight ahead to its right");
|
||||
t = InEye(Turn(10.0f, 0.0f, 1.0f, 0.0f), Turn(10.0f, 0.0f, 1.0f, 0.0f));
|
||||
CheckNear(t.x, 0.0f, "gaze along the canted eye: x");
|
||||
CheckNear(t.y, 0.0f, "gaze along the canted eye: y");
|
||||
|
||||
// The head's own turn cancels out: only the gaze relative to the eye counts.
|
||||
const Quaternion head = Turn(70.0f, 0.0f, 1.0f, 0.0f);
|
||||
const Quaternion look = Turn(70.0f + 12.0f, 0.0f, 1.0f, 0.0f);
|
||||
t = InEye(look, head);
|
||||
CheckNear(t.x, -std::tan(12.0f * kDegrees), "a turned head: x");
|
||||
|
||||
// Sideways or behind is no point of the image; an unnormalised quaternion still works.
|
||||
Check(!InEye(Turn(85.0f, 0.0f, 1.0f, 0.0f), {}).valid, "85 degrees off is invalid");
|
||||
Check(!InEye(Turn(180.0f, 0.0f, 1.0f, 0.0f), {}).valid, "behind is invalid");
|
||||
Quaternion scaled = Turn(15.0f, 1.0f, 0.0f, 0.0f);
|
||||
scaled = {scaled.x * 3.0f, scaled.y * 3.0f, scaled.z * 3.0f, scaled.w * 3.0f};
|
||||
t = InEye(scaled, {});
|
||||
CheckNear(t.y, std::tan(15.0f * kDegrees), "unnormalised quaternion");
|
||||
Check(InEye({0.0f, 0.0f, 0.0f, 0.0f}, {}).valid, "a zero quaternion reads as identity");
|
||||
|
||||
std::printf("vr_eye_gaze_tests: all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user