Locomotion and comfort

You never move the camera in WebXR. You replace the reference space with an offset copy of itself — and the difference between doing that instantly and doing it gradually is the difference between a comfortable app and a nauseating one.

Teleport, smooth motion, and the moving origin

The orange ring on the floor is the reference space origin; the headset is a child of it, offset by whatever the tracking system reports. Switch between teleport and smooth and watch what moves — the origin, never the head's offset from it.

This demo needs WebGL, which your browser did not provide. The explanation below covers the same material on its own.

Loading demo…

The camera is not yours to move

In a WebXR session the camera pose is written by the runtime every frame from the actual position of the headset on someone's actual head. Anything you assign to camera.position is overwritten before it is drawn, and in three.js specifically, renderer.xr owns the camera entirely once a session is presenting. Code that moves the camera works perfectly in the desktop preview and does nothing at all in the headset.

What you move instead is the reference space — the coordinate system the head pose is reported against. Shift that, and the head keeps its own tracked offset while the whole play area lands somewhere new. This is why the API for locomotion is a method on XRReferenceSpace and not on anything camera-shaped.

The method is getOffsetReferenceSpace, and the thing people find surprising is that it does not mutate anything. It returns a new reference space, and the transform you pass describes where the origin moves to, so the sign is the opposite of what "move the player forward" suggests. Moving the player one metre forward means moving the origin one metre backward relative to them.

Teleporting, and turning

Each locomotion step derives a new reference space from the current one and keeps it. The old space is not invalidated, but you stop using it — the space you pass to getViewerPose is what defines where the player is.

// The space every pose this frame is resolved against. Locomotion replaces it.
let referenceSpace = await session.requestReferenceSpace('local-floor');

function teleportTo(x, z) {
  // The transform moves the ORIGIN, so to put the player at (x, z)
  // the origin goes to (-x, -z). Getting this backwards is the single
  // most common bug in a first teleport implementation.
  const offset = new XRRigidTransform({ x: -x, y: 0, z: -z });
  referenceSpace = referenceSpace.getOffsetReferenceSpace(offset);
}

function snapTurn(degrees) {
  const half = (degrees * Math.PI) / 360;      // half-angle, in radians
  // A quaternion about the Y axis. Rotate about the player's head position,
  // not the origin, or a turn also flings them sideways.
  const orientation = { x: 0, y: Math.sin(half), z: 0, w: Math.cos(half) };
  referenceSpace = referenceSpace.getOffsetReferenceSpace(
    new XRRigidTransform({ x: 0, y: 0, z: 0 }, orientation),
  );
}

function onFrame(time, frame) {
  // Always the CURRENT space -- a stale capture from session start
  // silently ignores every teleport that happened since.
  const pose = frame.getViewerPose(referenceSpace);
  if (!pose) return;
  render(pose);
}

In three.js the same thing is spelled renderer.xr.setReferenceSpace(space), or more commonly by parenting the camera rig to a Group and moving the Group — which is the same idea expressed in the scene graph rather than in the WebXR API.

Why smooth locomotion makes people ill

Motion sickness in VR is a sensory conflict. Your eyes report that you are accelerating across a room; your inner ear reports that you are standing still. The brain treats that disagreement the way it treats poisoning, which is why the symptoms are nausea rather than confusion.

The strength of the illusion of self-motion — vection — is what determines how bad it gets, and it is driven mostly by optical flow in peripheral vision. That gives you the shape of every mitigation: reduce peripheral flow, or remove the continuous motion entirely. It also explains why the same app can be fine in a bare grey room and awful once you add pillars — static reference objects streaming past the edges of vision are exactly what produces vection.

Rotation is worse than translation, and the yaw axis is the worst of all. This is why snap turn exists as a separate comfort option even in apps that already teleport: a continuous turn produces strong flow across the entire visual field with no head movement to match it.

Acceleration is worse than constant velocity. If you do offer smooth motion, get to full speed quickly rather than easing in — a gentle ramp feels considerate and is measurably more uncomfortable than an abrupt one.

The comfort options worth having

None of these is exotic. Shipping VR applications converged on roughly this set, and users have learned to look for them.

Teleport
Instant relocation with no intermediate frames. No continuous optical flow means no vection and effectively no sickness. The cost is a break in spatial continuity, usually softened with a short fade to black.
Snap turn
Rotation in discrete steps, typically 30° or 45°. Same principle as teleport applied to yaw. Make the step size configurable: 45° is fewer inputs, 30° is easier to keep oriented with.
Comfort vignette
Darken the periphery while the player is moving. It removes the part of the visual field that drives vection while leaving the centre clear. Fade it in over a few frames — a vignette that pops is its own distraction.
A static rest frame
A cockpit, a nose reference, a grid that stays fixed to the head. Anything that does not move with the world gives the brain something to trust and measurably reduces the conflict.
Standing versus seated
A seated user cannot step out of the way, so anything that moves them is more uncomfortable. Respect the reference space you were given: bounded-floor implies room-scale, local implies they may be sitting.

What the demo above is doing

The orange ring is the reference space origin and the short line out of it is the origin's forward direction. The headset sits at a fixed offset above and beside it, drifting slightly the way a real tracked head does. Every locomotion step moves the ring; the headset's offset from the ring never changes, because that offset is the tracking data and is not yours to rewrite.

In teleport mode an arc is drawn from the controller to the destination and the origin jumps there when the dwell expires — no intermediate frames. Switch to smooth and the origin glides instead, at whichever speed you set, with the comfort vignette drawn in front of the visor while it moves.

The snap turn control is the interesting one to play with. At 0 the origin rotates continuously, which is the motion most likely to make a real user uncomfortable. At 30° or 45° the same rotation happens in discrete jumps and the intermediate angles are never rendered at all — which is the entire mechanism, and it is much easier to believe once you have watched the same turn happen both ways.

Choosing a locomotion scheme

There is no single right answer, but there is a wrong one: offering only smooth locomotion with no comfort options.

SchemeComfortPrecisionGood for
Teleport + snap turnHighCoarseMost applications. The safe default, and what users expect to find.
Smooth + vignette + snap turnMediumFineGames where continuous movement is the point; always paired with a teleport option.
Smooth, no mitigationLowFineSeated cockpit experiences with a strong static rest frame, and little else.
Room-scale only (no locomotion)HighestPhysicalAnything that fits in a bounded-floor play area. Nothing beats actually walking.
Grab / pull-the-worldHighFineScale-model and inspection apps; the user drags the world instead of moving through it.

Comfort ratings reflect the broad consensus of shipped VR titles as of 2026-09, not a controlled study. Individual sensitivity varies enormously — always offer the choice.

Mistakes that cost the most time

The first two produce code that looks correct on a desktop monitor and is broken the moment it runs on a head.

Moving camera.position
The runtime overwrites it from the real head pose every frame. Works in the preview, does nothing in the headset, and wastes an afternoon.
Reusing the original reference space
getOffsetReferenceSpace returns a new space. Keep passing the old one to getViewerPose and every teleport is silently discarded.
Getting the offset sign backwards
The transform moves the origin, not the player. Moving the player forward means moving the origin backward — the demo above jumps the wrong way if you flip it.
Rotating about the origin instead of the head
A snap turn that pivots around the play space origin also swings the player through an arc. Rotate about their current head position or they will be thrown sideways.
Easing into smooth motion
Acceleration drives sickness harder than velocity does. A slow ramp-up feels polite and is worse than snapping straight to full speed.
Shipping one locomotion scheme
Sensitivity varies by more than any tuning you can do. Comfort settings are an accessibility feature, not a preference screen.

Further reading

The reference space material is worth reading first — locomotion only makes sense once the origin is a thing you can picture.