The "vec3 is 4 floats" thing was consistently confusing to people. It's
reverted everywhere except for Curve.
maf now has full sets of methods for vec2/vec3/vec4, for consistency.
Vector bindings now use luax_readvec* helper functions for the
number/vector variants, and use maf for most functionality, which cleans
things up a lot.
Some compile fixes and a rename from gpu_wgpu to gpu_web, since wgpu
refers to a specific implementation of WebGPU and I'm really bad at
typing it for some reason.
- Adds Pass:setViewCull to enable/disable frustum culling.
- Renames Pass:setCullMode to Pass:setFaceCull (with backcompat).
Some stuff currently missing:
- Text is not culled, but should be.
- VR view frusta are not merged yet.
It's important that the bits for the vector type occupy the least
significant bits, so that vectors can be distinguished from pointer
lightuserdata.
When the vector pool was expanded, this broke, causing e.g. Blob
pointers to exhibit undefined behavior when trying to use them as
vectors.
tbh I still don't understand the union/bitfield memory layout.
Quest added a thing where they emulate grip pose when hand tracking is
active. This is actually pretty cool, and maybe LÖVR should do it too
on other runtimes, but it messed up the Quest hand mesh animation, for
some complicated reasons:
- Previously, getPose('hand/*') was returning the wrist pose because
LÖVR fell back to hand tracking data when the controller wasn't
tracked.
- Because of this, coupled with the fact that hand/controller models are
expected to be drawn at the hand pose, hand meshes were animated such
that the root node was located at the wrist pose.
- When Oculus added grip pose emulation for hand tracking, it caused a
discrepancy:
- Hand meshes were still being animated relative to their wrist pose
- getPose was now returning grip-style poses
- This resulted in hand meshes being off by approximately 90 degrees.
The fix is to locate skeletal joints relative to the grip pose when
animating Oculus hand meshes, and to place the origin/wrist at its real
pose instead of assuming it's the origin.
Origin type used to be a query-able property of the VR system that
indicated whether the tracking was roomscale or seated-scale.
The t.headset.offset config value could be used to design an
origin-agnostic experience, which by default shifted content up 1.7
meters when tracking was seated-scale. That way, stuff rendered at
y=1.7m was always at "eye level". It worked pretty well.
It's getting replaced with a t.headset.seated flag.
- If seated is false (the default), the origin of the coordinate space
will be on the floor, enabling the y=1.7m eye level paradigm. If
tracking is not roomscale, a floor offset of 1.7m will be emulated.
- If seated is true, the origin of the coordinate space will be y=0
at eye level (where the headset was when the app started). This is
the case on both roomscale and seated-scale tracking.
So basically 'seated' is an opt-in preference for where the app wants
its vertical origin to be.
One advantage of this is that it's possible to consistently get a y=0
eye level coordinate space, which was not possible before. This makes
it easier to design simpler experiences that only need to render a
floating UI and don't want to render a full environment or deal with
offsetting everything relative to a 'floor'. This also makes it easier
to implement hybrid VR+flatscreen experiences, because the camera is at
y=0 when the headset module is disabled.
The opt-in nature of the flag, coupled with the fact that it is
consistent across all types of tracking and hardware, is hopefully a
more useful design.
You can do lovr.headset.getPose('floor') to get the offset of the stage
relative to the local origin if you want to draw something at the center
of the play area.
Also lovr.headset.isTracked('floor') basically tells you if it's roomscale.
If the very first graphics-related thing done in a frame is drawing a
model, the reanimation logic would skip because a new frame hasn't
started yet. lovrModelAnimateVertices needs to unconditionally start a
new frame. (Previously, a new frame was guaranteed to be started
because all passes were temporary, but this is no longer the case).