Skip to main content
Version: 3.5.x

Navigation

The Navigation module lets an Inverse3 cursor move the application's workspace — sometimes called rate-control locomotion or "gamepad-like drift". Instead of the cursor hitting a hard wall at the edge of the physical workspace, it enters a soft shell where its distance from a virtual centre is mapped to a velocity that slides the whole workspace. The further the cursor pushes, the faster the scene scrolls.

The primary (and currently only) behaviour is Bubble Navigation. The bubble shape is defined using an SDF primitive — see What is an SDF? for the concept.


Bubble Navigation — concept

A virtual bubble is anchored around a centre point in the device's mount space. The cursor experiences three concentric zones:

┌──────────────────────────────────────────┐
│ WALL ZONE │ cursor beyond outer shell
│ ┌────────────────────────────────────┐ │
│ │ VELOCITY ZONE │ │ soft shell → scene moves
│ │ ┌──────────────────────────────┐ │ │
│ │ │ DEAD ZONE (inside) │ │ │ no scene movement
│ │ │ │ │ │
│ │ │ ● centre │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ bubble surface │ │
│ └────────────────────────────────────┘ │
│ outer boundary │
└──────────────────────────────────────────┘
ZoneCursor positionBehaviour
Dead zoneInside the bubbleNo navigation. Light damping. You can manipulate the scene freely.
Velocity zoneBetween the surface and the outer boundaryDistance → velocity curve moves the workspace in the cursor's direction.
Wall zonePast the outer boundaryA hard spring pushes the cursor back in and navigation velocity saturates.

A spring-damper haptic force is applied across all three zones — you feel the surface, the drift shell, and the outer wall.


Quick start — enable bubble navigation

Navigation is a persistent, one-shot configuration — send it once and it stays active until you explicitly stop it, restart the service, or close the session.

Start (minimal — default rounded-box bubble)

{
"inverse3": [
{
"device_id": "04C3",
"configure": {
"navigation": { "mode": "bubble" }
}
}
]
}

Stop

{
"inverse3": [
{
"device_id": "04C3",
"configure": {
"navigation": { "mode": "disabled" }
}
}
]
}

Other HTTP routes

MethodPathPurpose
GET/{type}/{id}/config/navigation?session=<expr>Current navigation config + state
POST/{type}/{id}/config/navigation?session=<expr>Start or update navigation
DELETE/{type}/{id}/config/navigation?session=<expr>Stop navigation

Bubble shape catalogue

The bubble's dead zone is described by a signed-distance-function (SDF) shape. Different shapes give different navigation feels — a rounded box (the default) for a comfortable rectangular rest zone, a sphere for isotropic drift, an ellipsoid to favour certain axes, a capsule for corridors.

Rounded box (default)

The default dead zone is a flat, slightly taller-than-deep volume — tuned to feel comfortable on both Inverse3 and MInverse:

{
"shape": {
"primitive": "rounded_box",
"parameters": { "b": { "x": 0.05, "y": 0.02, "z": 0.03 }, "r": 0.01 }
}
}

Sphere — isotropic drift

{
"shape": {
"primitive": "sphere",
"parameters": { "r": 0.05 }
}
}

Ellipsoid — wider in X/Z than Y

Use when horizontal drift should feel looser than vertical:

{
"shape": {
"primitive": "ellipsoid",
"parameters": { "a": { "x": 0.06, "y": 0.03, "z": 0.06 } }
}
}

Capsule — elongated corridor

Two end points a/b plus a radius r:

{
"shape": {
"primitive": "capsule",
"parameters": {
"a": { "x": 0.0, "y": -0.03, "z": 0.0 },
"b": { "x": 0.0, "y": 0.03, "z": 0.0 },
"r": 0.04
}
}
}

Full example with custom sizing and motion feel:

{
"inverse3": [
{
"device_id": "04C3",
"configure": {
"navigation": {
"mode": "bubble",
"bubble": {
"shape": {
"primitive": "ellipsoid",
"parameters": { "a": { "x": 0.06, "y": 0.03, "z": 0.06 } }
},
"velocity_zone_width": 0.025,
"max_velocity": 1.0,
"velocity_ease": "quadratic_in"
}
}
}
}
]
}

Velocity curve

The velocity-zone distance is mapped through an easing curve before being turned into workspace velocity. Pick the easing that matches how you want the scene to accelerate as the user pushes further into the shell.

velocity_easeFeelWhen to use
linearConstant rampPredictable, good default
quadratic_inSlow start, faster pushPrecise near the surface, fast long travel
cubic_inVery slow startVery precise, long travel ramps up
sine_outSmooth fast start, plateaus near outerResponsive, caps gently
quadratic_outFast at entry, comfortable long-distanceFast nudge, comfortable cruising
{ "velocity_ease": "quadratic_in", "max_velocity": 1.5 }

Centre behaviour

The bubble's centre is configured through a nested center object with four fields — position, relative, follow, speed — replacing the older flat layout made of center, center_enabled, center_mode, and center_drift_speed.

{
"center": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"relative": true,
"follow": false,
"speed": 0.02
}
}

Where the centre is anchored

position and relative control the initial centre location at activation:

relativeSemantics of position
true (default)position is an offset from the cursor — the initial centre is cursor + position. With the default (0, 0, 0), the bubble spawns exactly on the cursor.
falseposition is an absolute point in application space — it is converted to mount space on ingress and converted back to application space on egress, so round-trip values match.

How the centre moves afterwards

follow and speed control the per-tick behaviour once the bubble is active:

followspeedBehaviourPrevious equivalent
false (default)ignoredCentre stays where it was initialised — fixed centre.center_mode: "fixed"
true> 0Centre drifts toward the cursor at speed m/s while the cursor is inside.center_mode: "auto_follow" (with center_drift_speed)
true0.0Centre snaps to the cursor each tick — teleport feel.center_mode: "track_cursor"
Default changed in 3.5

The default centre behaviour is now fixed (follow = false). Earlier versions drifted toward the cursor by default (auto_follow). Set center.follow = true to restore the old drift behaviour, and tune center.speed to match the old value of center_drift_speed.

When follow = true and the cursor pushes past the outer boundary, the centre tracks the cursor regardless of speed, to keep the bubble around it and prevent the cursor from escaping.


Collision response

The bubble reacts to external cursor forces exceeding collision_detection.force_threshold:

  • The velocity zone temporarily inflates (up to collision_detection.inflate_ratio times velocity_zone_width) to give the user more room to manoeuvre around the obstacle.
  • When collision_detection.enabled is true (the default), the bubble centre refuses to drift in the direction of the collision force, preventing the user from dragging the workspace into a hard wall.

Collision detection uses hysteresis to avoid flicker around the threshold: once a collision is active it stays active until |ext_force| drops below the product collision_detection.force_threshold × collision_detection.exit_ratio. Lower the exit_ratio to widen the hysteresis band (stickier collisions); set it to 1.0 to disable hysteresis and use a single threshold.

{
"collision_detection": {
"enabled": true,
"force_threshold": 0.5,
"inflate_ratio": 2.0,
"exit_ratio": 0.7
}
}

Parameters reference

Most commonly tuned

ParameterDefaultDescription
shape{rounded_box, r=0.01, b=(0.05,0.02,0.03)}SDF shape defining the dead zone
velocity_zone_width0.03 mThickness of the rate-control shell
max_velocity0.5 m/sMaximum navigation velocity
velocity_ease"quadratic_in"Distance → velocity easing curve
center.followfalseWhether the centre tracks the cursor
center.speed0.02 m/sDrift rate when follow = true

Full parameter list

FieldTypeDefaultDescription
center.positionvec3(0, 0, 0)Initial centre — cursor offset when relative = true, absolute app-space point when false
center.relativebooltruetrue: position is an offset from the cursor. false: absolute app-space point, converted to mount space at the boundary
center.followboolfalsefalse: centre stays fixed at its initialised position. true: centre tracks the cursor
center.speedfloat0.02Drift speed (m/s) when follow = true; 0.0 snaps to the cursor every tick
shapeshape{rounded_box, r=0.01, b=(0.05,0.02,0.03)}Dead-zone SDF shape
velocity_zone_widthfloat0.03Width of the rate-control shell (m)
max_velocityfloat0.5Max navigation velocity (m/s)
velocity_easestring"quadratic_in"Distance → velocity easing
reset_velocity_on_entrybooltrueZero the accumulated speed when the cursor re-enters the velocity zone
bump_widthfloat0.005Tactile bump at the surface (m)
bump_stiffnessfloat200.0Surface bump spring constant
spring_innerfloat0.0Spring at the bubble centre
spring_surfacefloat10.0Spring at the surface
spring_outerfloat15.0Spring at the outer boundary
wall_stiffnessfloat700.0Hard-wall spring past the outer boundary
damping_innerfloat0.1Damping at the centre
damping_surfacefloat4.0Damping at the surface
damping_outerfloat7.0Damping at the outer boundary
rotation_enabledboolfalseApply the workspace rotation to the navigation direction
scale_enabledboolfalseApply the workspace scale to the navigation velocity
collision_detection.enabledbooltrueBlock navigation in the collision direction
collision_detection.force_thresholdfloat1.0External force magnitude to enter a collision (N)
collision_detection.inflate_ratiofloat2.0Multiplier for the velocity-zone width during a collision (must be ≥ 1.0)
collision_detection.exit_ratiofloat0.7Hysteresis ratio — stay in collision until |ext_force| < force_threshold × exit_ratio. Range (0, 1]; 1.0 disables hysteresis
Fields hidden in 3.5

Two feature groups are omitted from the 3.5 JSON schema while their implementations are reshaped for 3.6:

  • Avatar-boundary clamping: the fields avatar_boundary_enabled, avatar_boundary, and avatar_boundary_hysteresis.
  • Workspace bounding: the fields workspace_bounded, workspace_transition_speed, and workspace_transition_ease.

The tick code still compiles and runs with defaults, but clients cannot read or write those fields via HTTP or WebSocket. In 3.6 the avatar boundary will return as a top-level bounds peer of the bubble object with a full placement transform; workspace bounding will return once the centre-jitter investigation lands.

Validation rules

  • velocity_zone_width > 0
  • 0 ≤ bump_width < velocity_zone_width
  • max_velocity > 0
  • bump_stiffness ≥ 0
  • spring_inner ≥ 0 and spring_inner ≤ spring_surface ≤ spring_outer
  • wall_stiffness ≥ 0
  • damping_inner ≥ 0 and damping_inner ≤ damping_surface ≤ damping_outer
  • collision_detection.force_threshold > 0
  • collision_detection.inflate_ratio ≥ 1.0
  • collision_detection.exit_ratio in (0, 1]
  • center.speed ≥ 0

A POST or configure.navigation with invalid parameters is rejected and an invalid-value event is emitted; the previous configuration remains active.


Events

Event nameFired when
navigation-startedNavigation is activated on a device
navigation-updatedNavigation config is updated while already active
navigation-stoppedNavigation is stopped (explicit disable, DELETE, or session close)
invalid-valueA navigation config is rejected by validation

Known limitations

  • Workspace bounding and avatar boundaries hidden in 3.5: both feature groups are absent from the JSON schema while they are reshaped for 3.6. See the note above under Parameters reference.
  • Non-uniform scale + rotation: when both rotation_enabled and scale_enabled are on, the velocity direction is slightly inaccurate — rotation is not applied to the scale axes.
  • Per-device size scaling: bubble sizes (radius, zone width) are not scaled by the device's physical scale factor — MInverse and Inverse3 use the same absolute sizes, which may feel different on each.