Examples

DriftCapture

A synthesised clip of a room fused into a surface, proposed as entities, that balls land on.

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

Open on its own · Source

DriftCapture is the chapter that walks through it.

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

Source

main.ts

examples/capture/main.ts
/**
 * A clip of a room turned into a surface you can drop things on, on this device, with no service.
 *
 * The clip is synthesised: a ring of frames ray cast from a scene of a floor and four boxes, each
 * frame with its depth, as a depth model would give for a real video. The frames are fused into a
 * volume, its surface is marched out as a mesh and brought to a triangle budget, the mesh is cut
 * into surfaces and each surface is proposed as scenery or as somewhere a character could walk, and
 * the same mesh becomes a collider the balls land on.
 *
 * Fewer frames leave holes where nothing looked; paint the surfaces or the proposals to see what
 * the capture decided.
 */
import {
  collisionMesh,
  createVolume,
  decimate,
  fuseDepth,
  lookAt,
  marchVolume,
  proposeEntities,
  renderTestScene,
  segmentGeometry,
} from '@driftengine/capture';
import type { SurfaceView, TestScene } from '@driftengine/capture';
import { MeshBuilder, createEnvironment, hashToUnit } from '@driftengine/core';
import type { MeshData, MeshHandle, Vec3 } from '@driftengine/core';
import {
  BODY_DYNAMIC,
  BODY_STATIC,
  PhysicsWorld,
  meshShape,
  sphereShape,
} from '@driftengine/physics';
import { createReadout } from '../common/readout';
import { controls, flag, openStage } from '../common/stage';

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

// #region clip
/** The room the clip is of: a floor, a table, two crates stacked, and a cabinet. */
const ROOM: TestScene = {
  boxes: [
    { min: [-1.6, 0, -0.8], max: [-0.4, 0.8, 0.4], seed: 1 },
    { min: [0.3, 0, -1.2], max: [1.5, 0.5, 0.2], seed: 2 },
    { min: [0.5, 0.5, -0.9], max: [1.1, 1.1, -0.3], seed: 5 },
    { min: [-0.5, 0, 0.9], max: [0.5, 1.4, 1.5], seed: 7 },
  ],
  planes: [{ normal: [0, 1, 0], offset: 0, seed: 3 }],
};

/** Frames from a ring of positions round the room, each with the depth along the camera's axis. */
const WIDTH = 96;
const HEIGHT = 72;
function clip(frames: number): SurfaceView[] {
  const views: SurfaceView[] = [];
  for (let i = 0; i < frames; i += 1) {
    const angle = (i / frames) * Math.PI * 2;
    const eye: [number, number, number] = [Math.cos(angle) * 4.5, 2.2, Math.sin(angle) * 4.5];
    const view = {
      width: WIDTH,
      height: HEIGHT,
      intrinsics: [80, 80, WIDTH / 2, HEIGHT / 2] as [number, number, number, number],
      worldToCamera: lookAt(eye, [0, 0.4, 0]),
    };
    const pixels = new Uint8Array(WIDTH * HEIGHT * 4);
    const depth = new Float32Array(WIDTH * HEIGHT);
    renderTestScene(ROOM, view, pixels, depth);
    views.push({ depth, coverage: depth.map((d) => (d > 0 ? 1 : 0)), camera: view });
  }
  return views;
}
// #endregion

// #region surface
/** Every frame fused into one volume, its surface marched out, and brought to a budget. */
function surfaceOf(views: readonly SurfaceView[], budget: number): MeshData {
  const volume = createVolume([72, 30, 72], [-2.5, -0.3, -2.5], 0.07);
  fuseDepth(views, volume);
  const marched = marchVolume(volume);
  return budget > 0 ? decimate(marched, budget) : marched;
}
// #endregion

// #region propose
/** Paint each triangle by the surface it belongs to, or by what the capture proposes it is. */
const WALKABLE: Vec3 = [0.35, 0.75, 0.4];
const SCENERY: Vec3 = [0.62, 0.6, 0.56];
function painted(mesh: MeshData, paint: string): MeshData {
  const regions = segmentGeometry(mesh, { creaseDegrees: 15 });
  const proposals = proposeEntities(regions);
  const colourOf = new Map<number, Vec3>();
  regions.forEach((region, index) => {
    const proposal = proposals[index];
    const colour: Vec3 =
      paint === 'regions'
        ? [
            0.3 + hashToUnit(index * 3) * 0.6,
            0.3 + hashToUnit(index * 3 + 1) * 0.6,
            0.3 + hashToUnit(index * 3 + 2) * 0.6,
          ]
        : paint === 'proposals' && proposal?.walkable === true
          ? WALKABLE
          : SCENERY;
    for (const triangle of region.triangles) colourOf.set(triangle, colour);
  });
  /* Three vertices a triangle, so neighbouring surfaces do not blend at their edges. */
  const count = mesh.indices.length / 3;
  const positions = new Float32Array(count * 9);
  const normals = new Float32Array(count * 9);
  const colors = new Float32Array(count * 9);
  for (let t = 0; t < count; t += 1) {
    const colour = paint === 'clay' ? SCENERY : (colourOf.get(t) ?? SCENERY);
    for (let k = 0; k < 3; k += 1) {
      const from = (mesh.indices[t * 3 + k] ?? 0) * 3;
      const to = (t * 3 + k) * 3;
      positions.set(mesh.positions.subarray(from, from + 3), to);
      normals.set(mesh.normals.subarray(from, from + 3), to);
      colors.set(colour, to);
    }
  }
  return {
    positions,
    normals,
    colors,
    emissive: new Float32Array(count * 3),
    indices: Uint32Array.from({ length: count * 3 }, (_, i) => i),
  };
}
// #endregion

let frames = Number(flag('frames', '24'));
let budget = Number(flag('budget', '4000'));
let paint = flag('paint', 'proposals');
let drawn: MeshHandle | null = null;
let triangles = 0;
const readout = createReadout(renderer, 1);

// #region collide
/** The same surface as a static body, and balls dropped onto it. */
let world = new PhysicsWorld();
const BALLS = 24;
let balls: number[] = [];
function collide(mesh: MeshData): void {
  world = new PhysicsWorld();
  const { positions, indices } = collisionMesh(mesh);
  world.addBody({ type: BODY_STATIC, shape: meshShape(positions, indices), friction: 0.6 });
  balls = Array.from({ length: BALLS }, (_, i) =>
    world.addBody({
      type: BODY_DYNAMIC,
      shape: sphereShape(0.12),
      x: (hashToUnit(i * 7) - 0.5) * 3,
      y: 2 + i * 0.25,
      z: (hashToUnit(i * 7 + 1) - 0.5) * 3,
      restitution: 0.4,
    }),
  );
}
// #endregion

function capture(): void {
  const mesh = surfaceOf(clip(frames), budget);
  if (drawn !== null) renderer.disposeMesh(drawn);
  drawn = renderer.createMesh(painted(mesh, paint));
  triangles = mesh.indices.length / 3;
  collide(mesh);
  readout.set(0, `${frames} FRAMES  ${triangles} TRIANGLES`);
}
capture();

controls([
  {
    key: 'frames',
    label: 'frames',
    value: String(frames),
    options: ['6', '24'].map((n) => ({ text: n, value: n })),
    change: (value) => {
      frames = Number(value);
      capture();
    },
  },
  {
    key: 'budget',
    label: 'triangles',
    value: String(budget),
    options: [
      { text: 'as marched', value: '0' },
      { text: '4,000', value: '4000' },
    ],
    change: (value) => {
      budget = Number(value);
      capture();
    },
  },
  {
    key: 'paint',
    label: 'paint',
    value: paint,
    options: ['clay', 'regions', 'proposals'].map((p) => ({ text: p, value: p })),
    change: (value) => {
      paint = value;
      capture();
    },
  },
]);

const ball = renderer.createMesh(
  new MeshBuilder().addSphere([0, 0, 0], 0.12, [0.95, 0.45, 0.2], 0, 16, 8).build(),
);
const env = createEnvironment({
  directionalDir: [0.4, 0.8, -0.45],
  directionalColor: [1.7, 1.6, 1.45],
  ambient: [0.33, 0.36, 0.43],
  ambientGround: [0.14, 0.13, 0.12],
});
const IDENTITY = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
const model = new Float32Array(IDENTITY);
let time = 0;
let dropped = 0;
let drops = 0;

stage.run({
  simulate(dt) {
    time += dt;
    world.step(dt);
    /* A ball that has come to rest or fallen off is dropped again from above. */
    dropped += dt;
    if (dropped > 0.4) {
      dropped = 0;
      drops += 1;
      const at = balls[drops % BALLS];
      if (at !== undefined) {
        /* From the drop count, never a random source, so the simulation replays. */
        world.setPosition(
          at,
          (hashToUnit(drops * 2) - 0.5) * 3,
          3,
          (hashToUnit(drops * 2 + 1) - 0.5) * 3,
        );
        world.setVelocity(at, 0, 0, 0);
      }
    }
  },
  render() {
    camera.fovYDeg = 50;
    camera.position[0] = Math.sin(time * 0.2) * 5.5;
    camera.position[1] = 3.2;
    camera.position[2] = Math.cos(time * 0.2) * 5.5;
    camera.lookAt(0, 0.5, 0);
    renderer.beginFrame([0.55, 0.6, 0.68]);
    renderer.bindMeshPass(camera, env);
    if (drawn !== null) renderer.drawMesh(drawn, IDENTITY);
    for (const body of balls) {
      model[12] = world.bodies.posX[body] ?? 0;
      model[13] = world.bodies.posY[body] ?? 0;
      model[14] = world.bodies.posZ[body] ?? 0;
      renderer.drawMesh(ball, model);
    }
    readout.draw(time);
    renderer.endFrame();
  },
});