b3d-controller

The casual way to read the standard controller. Drop a <tosi-b3d-controller> into a scene and it self-wires the whole unified input stack — keyboard/mouse, the on-screen glass gamepad, a hardware gamepad, and (in a headset) the XR controllers — then hands you a merged ControlInput every frame via drive. No bespoke key listeners: just read input.forward / input.turn / input.shoot / … and drive whatever you like.

Demo

Drive the rover with W/S and A/D (or the left stick). Same controls on keyboard, touch, and in VR, because it's the standard controller, not a demo hack.

import { b3d, b3dController, b3dSkybox } from 'tosijs-3d'
import { demoSun, orbitCam, patternGround } from 'tosijs-3d/demo-utils'

// A rover we drive around: left stick / WASD → move + turn. The controller merges keyboard,
// the on-screen glass pad, and any hardware/XR pad, and hands `drive` the result each frame.
let rover // set in sceneCreated
const controller = b3dController({
  mapping: 'biped',
  drive(input, dt) {
    if (!rover) return
    // The biped mapping sends A/D and left-stick X to STRAFE and the right
    // stick to TURN; a rover has no sideways, so either one steers it.
    const steer = Math.max(-1, Math.min(1, input.turn + input.strafe))
    rover.rotation.y += steer * dt * 2.4 // turn (A/D · either stick)
    const step = input.forward * dt * 7 // drive (W/S · left stick Y)
    rover.position.x += Math.sin(rover.rotation.y) * step
    rover.position.z += Math.cos(rover.rotation.y) * step
  },
})

const scene = b3d(
  {
    gamepad: 'left_stick', // glass gamepad shows only what this demo uses
    sceneCreated(el, BABYLON) {
      orbitCam(el, { radius: 16, betaDeg: 52.9, target: [0, 0.5, 0] })
      rover = BABYLON.MeshBuilder.CreateBox('rover', { width: 1.2, height: 0.7, depth: 1.9 }, el.scene)
      rover.position.y = 0.5
      const mat = new BABYLON.StandardMaterial('rover-mat', el.scene)
      mat.diffuseColor = new BABYLON.Color3(0.85, 0.45, 0.2)
      rover.material = mat
      el.register?.({ meshes: [rover] }) // cast a shadow on the ground
    },
  },
  demoSun(),
  b3dSkybox({ timeOfDay: 10 }),
  patternGround({ size: 40 }),
  controller,
)
preview.append(scene)
tosi-b3d { width: 100%; height: 100%; }

It's a b3d-controllable with no body of its own, so it also respects scene input focus: on a page with several live demos, only the scene you're hovering over or interacting with receives input, so one gamepad does not drive them all.

Attributes

Attribute Default Description
mapping 'biped' Which control scheme maps the gamepad to ControlInput: 'biped', 'car', or 'aircraft'
player false Set it when the controller is nested in an inputFocus and you want that manager, not its own wiring, to drive it
drive — (input: ControlInput, dt: number) => void, called each frame with the merged input (set in code / via the creator). Named drive, not onInput — on* props become DOM event listeners

Put it inside a <tosi-b3d-input-focus> only if you want that manager to drive it instead — on its own it wires input itself.