Capítulo 859 de 859

Chapter 859: VR — 3DOF/6DOF Point to Select (Manual)

Core Idea

For VR devices with an actual pointing controller, three.js exposes each controller as an Object3D (via renderer.xr.getController(i)) with selectstart/select/selectend events, letting you raycast from the controller's own transform instead of from the camera, and even drag objects by reparenting them onto the controller.

Key Concepts

  • renderer.xr.getController(i): returns an Object3D tracking one controller's position/orientation (three.js handles both single-3DOF and dual-6DOF controller setups); a simple 3D line parented to it visualizes the pointing direction.
  • selectstart / select / selectend: controller events for when the user starts pressing, is pressing, and releases the controller's main button.
  • Raycasting from the controller, not the camera: unlike mouse picking (from the camera through the screen) or gaze picking (from the camera's facing direction), controller-based picking builds its ray from the controller's own world transform.
  • Separating pickable objects from pointer visuals: parenting raycastable objects under a dedicated Object3D (separate from the pointer-line objects) keeps the raycaster from accidentally picking its own visualization lines.
  • Wrapping controller logic in a custom EventDispatcher: extending three.js's EventDispatcher lets a helper class translate low-level per-controller select events into one clean high-level select event carrying which object was targeted, simplifying app code.
  • Object3D.attach() for dragging: on selectstart, reparenting a selected object onto the controller with attach() (which preserves world position/orientation during the reparent) makes it move with the controller; reparenting it back to the scene on selectend releases it — the basis for simple 6DOF object manipulation.

Code Examples

class ControllerPickHelper extends THREE.EventDispatcher {
  constructor(scene) {
    super();
    this.raycaster = new THREE.Raycaster();
    this.controllers = [];
    for (let i = 0; i < 2; ++i) {
      const controller = renderer.xr.getController(i);
      controller.addEventListener("select", (event) => {
        const selectedObject = this.controllerToObjectMap.get(event.target);
        if (selectedObject) this.dispatchEvent({ type: "select", controller: event.target, selectedObject });
      });
      scene.add(controller);
      this.controllers.push(controller);
    }
  }
}
  • What it demonstrates: wrapping per-controller select events in a custom EventDispatcher subclass so the app can listen for one unified select event carrying the actually-targeted object.

Key Takeaways

  1. renderer.xr.getController(i) gives you an Object3D per controller with position/orientation and selectstart/select/selectend events.
  2. Controller-based picking raycasts from the controller's own transform, not the camera — a different pick origin than mouse or gaze picking.
  3. Keep pickable scene objects under a separate parent from pointer-visualization lines so the raycaster doesn't hit its own visuals.
  4. Object3D.attach() reparents an object while preserving its world transform, making it the simple building block for "pick up and move" interactions with a 6DOF controller.

Connects To

  • VR - Look to Select (manual): the gaze-based picking approach this lesson extends to controller-based picking for devices that have one.
  • Picking (manual): the general Raycaster-based picking pattern reused here with a different ray origin.