Skip to content

lua-api: expose the eye field of view in degrees - #435

Open
Remleo wants to merge 1 commit into
praydog:masterfrom
Remleo:lua-projection-fov
Open

Remleo wants to merge 1 commit into
praydog:masterfrom
Remleo:lua-projection-fov

Conversation

@Remleo

@Remleo Remleo commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Adds vr.get_projection_fov(eye). Returns a flat table: left, right, up, down as
signed angles, plus horizontal and vertical totals.

What it is for

The headset decides the real field of view, the game keeps believing its own. That is fine
until the game draws its first person geometry through a separate field of view, which is
common: the weapon in view is often rendered with its own FOV so it does not distort at wide
camera angles. That value stays at whatever the flat game picked, so in VR the weapon does not
match the world, and it drifts against it when you move.

In The Outer Worlds 2 the pawn has a WeaponFOV function, and feeding it the headset's real
horizontal FOV is what fixes the weapon. My script does:

local f = vr.get_projection_fov(vr, 0)
pawn:WeaponFOV(f.horizontal, false)

A hardcoded number does not work here. The advertised figure for a headset is binocular, the
union of both eyes, while each eye gets an asymmetric cone rotated outward. On my Quest 3 the
left eye reads left -54, right 40, so one view gets 94 horizontal, and 94 is the number the
game needs. It also changes when the user touches the projection settings in the UEVR menu, so
a script has to re-read it rather than cache it once.

Why not just read the matrix

get_ue_projection_matrix already carries this. But getting angles out of it means knowing how
the matrix is laid out:

m[0][0] = 2/(r-l)     m[2][0] = -(r+l)/(r-l)
m[1][1] = 2/(t-b)     m[2][1] = -(t+b)/(t-b)

with l and b negative, which inverts to r = (1 - m[2][0]) / m[0][0] and
l = (-1 - m[2][0]) / m[0][0], same shape vertically. That is fine to do once, less fine to
repeat in every script that wants an angle. So it is done here, and the halves are kept
separate rather than averaged, because asymmetric projections are the normal case in VR.

Returns nil if m[0][0] or m[1][1] is zero, so a script does not get a bogus angle out of an
uninitialised projection.

Tested

Release build, The Outer Worlds 2 on a Quest 3 over OpenXR. Straight from my log:

HMD projection: horizontal 94.00, vertical 99.00 (left -54.00 right 40.00 up 44.00 down -55.00)
WeaponFOV -> 94.00 (from the HMD projection)

Feeding horizontal into WeaponFOV lines the weapon up with the world.

The headset decides the real field of view while the game keeps believing its own. A game that draws its first person geometry through a separate field of view has to be told the real one, otherwise that geometry is distorted and drifts against the world. This is what the weapon looks wrong in VR problem comes down to in The Outer Worlds 2, where the pawn has its own WeaponFOV.

get_ue_projection_matrix already carries the information, but recovering angles from it means knowing how the matrix is laid out, so the conversion is done here once rather than in every script that needs it.

Returns a flat table with left, right, up and down as signed angles plus the horizontal and vertical totals. The halves are kept separate because asymmetric projections are normal in VR: on a Quest 3 one eye reads left -54 and right 40, and averaging that away loses the part a script needs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant