Skip to content

OpenXREyeTracking.kt

src/jvmMain/kotlin/afield/graphics/vulkan/targets/OpenXREyeTracking.kt · 174 lines

package afield.graphics.vulkan.targets

import afield.math.linalg.Quat
import afield.math.linalg.Vec3
import afield.math.linalg.Vector2f
import org.lwjgl.openxr.XR10.*
import org.lwjgl.openxr.XrSpaceLocation

Plain state holders for OpenXR eye tracking: the combined gaze of XR_EXT_eye_gaze_interaction and the per-eye gaze, pupil and geometry streams of XR_HTC_eye_tracker.

Each holder is filled in place on the renderer's queue thread and carries its own validity flags, so read EyeGazeState.poseValid or the per-stream flags of HtcEyeState before using a value.

class EyeGazeState

Combined gaze from XR_EXT_eye_gaze_interaction, in the target's LOCAL reference space (metres, +Y up, -Z forward). Updated in place on the renderer's queue thread. Numeric pose values retain the last valid observation; check poseValid before using them.

class EyeGazeState {

val position = Vec3()

Where the combined gaze ray starts, in LOCAL space, in metres. Only written while positionValid.

val orientation = Quat()

Which way the gaze points, as a rotation of local -Z. Only written while orientationValid.

var isActive

False when the runtime reported no gaze action at all this frame, so nothing else here is current.

    var isActive = false
        internal set

var positionValid

The runtime gave a usable position in the last frame it was asked.

    var positionValid = false
        internal set

var orientationValid

The runtime gave a usable orientation in the last frame it was asked.

    var orientationValid = false
        internal set

var positionTracked

The position is being actively tracked rather than inferred. Valid but untracked is the runtime's best guess from an older observation, which is usable and should be trusted less.

    var positionTracked = false
        internal set

var orientationTracked

The orientation is being actively tracked rather than inferred — see positionTracked.

    var orientationTracked = false
        internal set

var sampleTime: Long

OpenXR time in nanoseconds at which the pose is expressed; zero when unavailable.

    var sampleTime: Long = 0
        internal set

val poseValid: Boolean get() = isActive && positionValid && orientationValid

isActive with both halves of the pose valid — the one check to make before reading the pose.

val isTracked: Boolean get() = poseValid && positionTracked && orientationTracked

False for a valid but degraded/untracked pose.

fun direction(out: Vec3): Boolean

Copies the gaze's forward (-Z) direction into out, only when the whole pose is valid.

    fun direction(out: Vec3): Boolean {
        if (!poseValid) return false
        orientation.transform(out.set(0f, 0f, -1f))
        return true
    }

internal fun invalidate()

    internal fun invalidate() {
        isActive = false
        positionValid = false
        orientationValid = false
        positionTracked = false
        orientationTracked = false
        sampleTime = 0
    }

internal fun update(location: XrSpaceLocation, time: Long)

    internal fun update(location: XrSpaceLocation, time: Long) {
        val flags = location.locationFlags()
        positionValid = flags and XR_SPACE_LOCATION_POSITION_VALID_BIT.toLong() != 0L
        orientationValid = flags and XR_SPACE_LOCATION_ORIENTATION_VALID_BIT.toLong() != 0L
        positionTracked = positionValid && flags and XR_SPACE_LOCATION_POSITION_TRACKED_BIT.toLong() != 0L
        orientationTracked = orientationValid && flags and XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT.toLong() != 0L
        val pose = location.pose()
        if (positionValid) position.set(pose.`position$`().x(), pose.`position$`().y(), pose.`position$`().z())
        if (orientationValid) orientation.set(
            pose.orientation().x(), pose.orientation().y(), pose.orientation().z(), pose.orientation().w()
        )
        sampleTime = if (poseValid) time else 0
    }
}

class HtcEyeState

One eye's independent observations from XR_HTC_eye_tracker. Check each validity flag.

class HtcEyeState {

val position = Vec3()

Gaze pose in the target's LOCAL reference space, in metres. Forward is local -Z.

val orientation = Quat()

Which way this eye points, as a rotation of local -Z. Written together with position, under gazeValid.

var gazeValid

This eye's position and orientation came through in the last gaze query.

    var gazeValid = false
        internal set

var pupilDiameterValid

pupilDiameter came through in the last pupil query. Independent of pupilPositionValid.

    var pupilDiameterValid = false
        internal set

var pupilPositionValid

pupilPosition came through in the last pupil query. Independent of pupilDiameterValid.

    var pupilPositionValid = false
        internal set

var pupilDiameter

Pupil diameter in millimetres (unlike the gaze position).

    var pupilDiameter = 0f
        internal set

val pupilPosition = Vector2f()

Normalized sensor coordinates [0, 1], +X right, +Y up.

var geometricValid

eyeOpenness, eyeWide and eyeSqueeze came through in the last geometry query, as one group.

    var geometricValid = false
        internal set

var eyeOpenness

How open the eye is, 0 shut to 1 open. Written under geometricValid.

    var eyeOpenness = 0f
        internal set

var eyeWide

How wide the eye is held open past its resting opening, 0 to 1. Written under geometricValid.

    var eyeWide = 0f
        internal set

var eyeSqueeze

How tightly the eye is squeezed shut, 0 to 1. Written under geometricValid.

    var eyeSqueeze = 0f
        internal set

internal fun invalidate()

    internal fun invalidate() {
        gazeValid = false
        pupilDiameterValid = false
        pupilPositionValid = false
        geometricValid = false
    }
}

class HtcEyeTrackingState

HTC per-eye gaze, pupil and geometry observations, updated in place on the renderer's queue thread. The three streams have independent validity and OpenXR timestamps (nanoseconds). A failed stream does not suppress the other streams. Numeric values retain the last valid observation; timestamps reset to zero when a query fails or a frame is skipped.

class HtcEyeTrackingState {

val left = HtcEyeState()

The left eye's observations. Each of its three streams carries its own validity flag.

val right = HtcEyeState()

The right eye's observations, read from the same three queries as left.

var gazeTime: Long

OpenXR time in nanoseconds of the gaze poses in left and right; zero when that query failed.

    var gazeTime: Long = 0
        internal set

var pupilTime: Long

OpenXR time in nanoseconds of the pupil diameters and positions; zero when that query failed.

    var pupilTime: Long = 0
        internal set

var geometricTime: Long

OpenXR time in nanoseconds of the openness, wide and squeeze values; zero when that query failed.

    var geometricTime: Long = 0
        internal set

internal fun invalidate()

    internal fun invalidate() {
        left.invalidate()
        right.invalidate()
        gazeTime = 0
        pupilTime = 0
        geometricTime = 0
    }
}