Examples

Picking

Hover and click: what is under the pointer.

Starts examples/picking in this page, on WebGPU where your browser has it.

Open on its own · Source

Picking is the chapter that walks through it.

From a checkout of the engine, npm run examples serves it at /picking/.

Source

main.ts

examples/picking/main.ts
/**
 * Twenty-five shapes on a table. The one under the pointer lights up; a click lifts it, and a second
 * click puts it down.
 *
 * Picking answers one question, what is under this pixel, against meshes registered for it. Hover,
 * press and drag are the game's: the engine says what was hit, where, and how far away.
 */
import { MeshBuilder, SceneNode } from '@driftengine/core';
import type { Vec3 } from '@driftengine/core';
import { DAYLIGHT, openStage } from '../common/stage';

const stage = await openStage({
  directionalShadows: true,
  sceneSamples: 4,
  outputTransform: 'aces',
});
const { renderer, camera, canvas } = stage;

const table = renderer.createMesh(
  new MeshBuilder().addBox([0, -0.2, 0], [6, 0.2, 6], [0.35, 0.3, 0.26]).build(),
);
const still = new SceneNode();
still.updateWorld();

// #region register
/** Each shape is drawn and registered for picking, with the same geometry and the same matrix. */
const shapeData = new MeshBuilder()
  .addBox([0, 0.4, 0], [0.4, 0.4, 0.4], [0.75, 0.75, 0.78])
  .build();
const shape = renderer.createMesh(shapeData);
const nodes: SceneNode[] = [];
const handles: number[] = [];
const lifted: boolean[] = [];
for (let row = 0; row < 5; row += 1)
  for (let column = 0; column < 5; column += 1) {
    const node = new SceneNode();
    node.setPosition((column - 2) * 2, 0, (row - 2) * 2);
    node.updateWorld();
    nodes.push(node);
    handles.push(renderer.registerPickable(shapeData, node.worldMatrix));
    lifted.push(false);
  }
// #endregion

// #region pointer
/** Where the pointer is, in the canvas's CSS box, and whether a click is waiting. */
let pointerX = -1;
let pointerY = -1;
let clicked = false;
canvas.addEventListener('pointermove', (event) => {
  const box = canvas.getBoundingClientRect();
  pointerX = event.clientX - box.left;
  pointerY = event.clientY - box.top;
});
canvas.addEventListener('pointerleave', () => {
  pointerX = -1;
});
canvas.addEventListener('click', () => {
  clicked = true;
});
// #endregion

const PLAIN: Vec3 = [1, 1, 1];
const HOVER: Vec3 = [1.6, 1.25, 0.6];
let time = 0;

stage.run({
  simulate(dt) {
    time += dt;
  },
  render() {
    camera.fovYDeg = 50;
    camera.position[0] = Math.sin(time * 0.1) * 3;
    camera.position[1] = 9;
    camera.position[2] = 10;
    camera.lookAt(0, 0, 0);

    // #region pick
    /* What is under the pointer: the nearest registered mesh along its ray, or nothing. */
    const hit = pointerX < 0 ? null : renderer.pickAt(camera, pointerX, pointerY);
    const hovered = hit === null ? -1 : handles.indexOf(hit.handle);
    if (clicked && hovered >= 0) {
      lifted[hovered] = !lifted[hovered];
      const node = nodes[hovered];
      node.setPosition(node.position[0], lifted[hovered] ? 1 : 0, node.position[2]);
      node.updateWorld();
      /* A pickable that moves is told where it went. */
      renderer.updatePickable(handles[hovered], node.worldMatrix);
    }
    clicked = false;
    // #endregion

    renderer.beginFrame([0.55, 0.6, 0.68]);
    renderer.bindMeshPass(camera, DAYLIGHT);
    renderer.drawMesh(table, still.worldMatrix);
    for (let i = 0; i < nodes.length; i += 1)
      renderer.drawMesh(shape, nodes[i].worldMatrix, 0, i === hovered ? HOVER : PLAIN);
    renderer.endFrame();
  },
});