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

VR.HasVrFlag boolean read-only

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.

VR.IsVrPresent boolean read-only

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.

VR.BodyCFrame CFrame

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.

VR.HeadCFrame CFrame read-only

Headset pose in body-local space. Driven by the VR backend each frame.

VR.IsLinked boolean read-only

true while the engine's camera is bound to the VR pipeline. Becomes false after UnlinkCamera().

VR.HeadMoved Signal<CFrame> read-only

Fires every time the backend reports a new head pose. Payload is the new body-local HeadCFrame.

VR:LinkCamera() → boolean method

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.

VR:UnlinkCamera() method

Release the camera back to non-VR control. Idempotent.

VR:HeadToWorldSpace() → CFrame method

Compose BodyCFrame with HeadCFrame and return the headset's world-space pose. Convenient inside HeadMoved handlers.

VR:Recenter() method

Re-anchor the play space so the current head pose becomes the origin. Common after a teleport or seated → standing transition.

VR:GetEyePose(side) → CFrame method

World-space pose of one eye, derived from HeadCFrame ± half-IPD. Useful for stereo-aware effects.

VR:GetControllers() → { Controller } method

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

.Side"Left" | "Right"read-only
.CFrameCFrameread-only

Body-local pose. Compose with VR.BodyCFrame for world space, or call :ToWorldSpace().

.Triggernumber 0…1read-only
.Gripnumber 0…1read-only
.ThumbstickVectorread-only

x = -1…1 horizontal, y = -1…1 vertical, z = 0. Poll directly or subscribe via OnInput.

.VelocityVectorread-only
.AngularVelocityVectorread-only
.IsConnectedbooleanread-only
.BatteryLevelnumber?read-only

0…1, or nil if the runtime doesn't report it.

.MovedSignal<CFrame>read-only

Fires on every pose update with the new body-local CFrame.

.OnInputSignal<string, number, "Begin"|"End">read-only

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

controller:ToWorldSpace()→ CFramemethod

Compose controller.CFrame with VR.BodyCFrame and return the world-space pose. Use this for raycasts, attaching weapons / hands, or triggering world interactions.

controller:Attatch(target)→ Attatchmentmethod

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()
controller:Vibrate(duration, frequency?, amplitude?)→ booleanmethod

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.

controller:StopVibration()method

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.

.OffsetCFrame

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.

.Side"Left" | "Right" | "Detached"read-only
.IsAlivebooleanread-only
attatchment:Destroy()method

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.

Soft-grab variant. For a grab that still pushes against world geometry (so the held thing can hit walls, players, etc.), don't use :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).
VirtualReality vs VR. The legacy 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.