VirtualReality
High-level VR import. Unlike the legacy
VR module, VirtualReality
always loads — on builds compiled without the
vr Cargo feature, HasVrFlag is
false and LinkCamera() is a no-op, so you
can ship one Luau codebase that gracefully degrades on non-VR
builds.
When the vr feature is enabled, the runtime pulls in
indite
(an OpenXR ↔ WGPU bridge) and binds head + controller poses
through to this API. Compile with
cargo build --release --features vr (requires
rustc ≥ 1.92, since indite pulls in
wgpu 28).
local VR = import("VirtualReality")
local primitives = import("Primitives")
if VR.HasVrFlag and VR.IsVrPresent then
VR.LinkCamera()
VR.HeadMoved:Connect(function()
print(VR:HeadToWorldSpace())
end)
VR.BodyCFrame = primitives.CFrame.new(vector.create(0, 1, 0))
local conns = VR.GetControllers() -- {Left, Right}
local Left = conns[1]
local Right = conns[2]
Left.Moved:Connect(function()
print(Left:ToWorldSpace())
end)
Right.OnInput:Connect(function(name, value, state)
if name == "Trigger" and state == "Begin" then
Right:Vibrate(0.05)
end
end)
end
API
Compile-time flag. true when the runtime was
built with --features vr. Always check this
before calling LinkCamera() — on
non-VR builds every VR-related call is a no-op.
Evaluated at import time. true if a VR
runtime is installed on the host (env-var probe +
Windows registry check for OpenXR's
ActiveRuntime). Always false
when HasVrFlag is false.
Player-rig body pose in world space. Write this to
teleport, snap-turn, or otherwise move the rig. The engine
composes BodyCFrame with the live head pose
onto the active camera each frame, so the user's actual
head motion is preserved on top of whatever you script.
Headset pose in body-local space. Driven by the VR backend each frame.
true while the engine's camera is bound to
the VR pipeline. Becomes false after
UnlinkCamera().
Fires every time the backend reports a new head pose.
Payload is the new body-local HeadCFrame.
Bind the engine's active camera to the VR pipeline. Returns
true on success (requires
HasVrFlag). After a successful link the
engine adds BodyCFrame to the headset pose
and writes the result to the camera each frame.
Release the camera back to non-VR control. Idempotent.
Compose BodyCFrame with HeadCFrame
and return the headset's world-space pose. Convenient inside
HeadMoved handlers.
Re-anchor the play space so the current head pose becomes the origin. Common after a teleport or seated → standing transition.
World-space pose of one eye, derived from
HeadCFrame ± half-IPD. Useful for
stereo-aware effects.
Returns a table with the pair of controller handles. Both
access patterns work:
conns[1] / conns[2] (Left / Right) or
conns.Left / conns.Right.
Controller
Returned by VR:GetControllers(). The same logical
controller is returned on every call; signals you connect remain
live across re-fetches.
Properties
Body-local pose. Compose with VR.BodyCFrame for world space, or call :ToWorldSpace().
x = -1…1 horizontal, y = -1…1 vertical, z = 0. Poll directly or subscribe via OnInput.
0…1, or nil if the runtime doesn't report it.
Fires on every pose update with the new body-local CFrame.
Fires (name, value, state) for trigger /
grip / thumbstick / button changes. name is
the OpenXR input identifier
("Trigger", "Grip",
"Thumbstick",
"ThumbstickClick",
"A", "B", "X",
"Y", "Menu", …).
state = "Begin" on press / cross-into-active,
"End" on release.
Methods
Compose controller.CFrame with
VR.BodyCFrame and return the world-space pose.
Use this for raycasts, attaching weapons / hands, or
triggering world interactions.
Attach a BasePart or DynMesh to this controller. The
runtime updates the held part's world CFrame each frame
(BodyCFrame * controller.CFrame * Attatchment.Offset)
so you don't have to drive it from a Heartbeat callback.
Returns an
Attatchment handle — set
its .Offset to position the part in the hand,
call :Destroy() to release. Physics-tracked
parts are auto-anchored while held and the
prior anchor state is restored on :Destroy(),
so the held thing stops obeying gravity / collisions for
the duration of the grab without any extra bookkeeping.
local sword = Renderable.BasePart("Cube")
sword.Size = vector.create(0.05, 0.05, 1.2)
local hold = Right:Attatch(sword)
hold.Offset = CFrame.new(vector.create(0, 0, -0.6)) -- forward in palm
-- later, release it:
hold:Destroy()
Fire a haptic pulse. duration in seconds;
frequency (Hz) and amplitude
(0…1) default to a short click pulse.
Returns true on devices that support haptics.
Cancel any in-flight pulses on this controller.
Attatchment
Returned by controller:Attatch(target). The runtime
drives the held part's CFrame each frame; you only touch the
attachment to retune the hand-offset or release it.
CFrame offset applied on top of the controller pose. Tweak this to position the held object in the hand — grip rotation, palm offset, tool tip-out-the-front, etc. Defaults to identity.
Detach the part. For physics-tracked parts the prior Anchored state is restored, so a dynamic object regains gravity / collisions and continues from wherever the controller dropped it.
:Attatch — use
PhysicsObject:SetPin
each frame against the controller's
:ToWorldSpace(). Attatch is "rigid grab" (the part
teleports with the hand); SetPin is "spring grab" (the part is
pulled toward the hand but can be stopped by obstacles).
VR module is now
behind the vr Cargo feature — on builds without
it, import("VR") fails. New code should prefer
VirtualReality, which always loads and gates its
own behavior on HasVrFlag.