Depth Estimation

Estimate depth from a photo or webcam with ONNX Runtime Web on WebGPU. Shade its GPU-resident output beside the input through a zero-copy vgpu buffer wrap.

Open fullscreen
/**
 * ORT-free presentation for the depth example.
 *
 * `scripts/render-example-thumbs.mjs` bundles this module for Node, so it must
 * never import ONNX Runtime Web, not even dynamically. Session orchestration
 * lives in `ort-runtime.ts`.
 *
 * The pipeline consumes a plain `array<f32>` and does not care whether those
 * floats came from a live model on the GPU or from the committed fixture, which
 * is exactly why thumbnails can be deterministic without a model.
 */
import type { Buffer, Compute, Effect, Gpu, Surface, Target } from 'vgpu';
import { compute as createCompute, effect as createEffect, frame as runFrame } from 'vgpu';
import {
  depthByteLength,
  depthElementCount,
  PRESENTATION_AUTO_RANGE,
  PRESENTATION_LOG_METRIC,
  type DepthModel,
} from './model-contract';
import reduceRangeWgsl from './reduce-range.wgsl';
import sideBySideWgsl from './side-by-side.wgsl';
 
/** Byte view for `Buffer.write`; narrows TypeScript's ArrayBufferLike generic. */
function asWriteData(view: Float32Array | Uint32Array | Uint8ClampedArray): Uint8Array<ArrayBuffer> {
  return new Uint8Array(view.buffer as ArrayBuffer, view.byteOffset, view.byteLength);
}
 
export interface SideBySidePipeline {
  /**
   * Draws one frame: the colour input on the left, its depth on the right.
   *
   * `depth` may be a non-owning wrap of ORT's output buffer, so this only reads
   * it inside the submitted pass and never retains it past the call.
   */
  draw(
    gpu: Gpu,
    output: Surface | Target,
    depth: Buffer,
    colour: Buffer,
    model: DepthModel,
    options?: { hasResult?: boolean },
  ): void;
  dispose(): void;
}
 
export function createSideBySidePipeline(gpu: Gpu, label = 'depth-estimation'): SideBySidePipeline {
  const effect: Effect = createEffect(gpu, sideBySideWgsl, { label: `${label}-view` });
  const reducer: Compute = createCompute(gpu, reduceRangeWgsl, { label: `${label}-range` });
  // Two u32 keys: the min and max of the current depth tensor.
  const range: Buffer = gpu.device.createBuffer({
    size: 8,
    usage: ['storage', 'copy_dst'],
    label: `${label}-range`,
  });
 
  return {
    draw(currentGpu, output, depth, colour, model, options = {}) {
      const hasResult = options.hasResult ?? true;
      const autoRange = model.presentation.mode === 'auto-range';
 
      if (hasResult && autoRange) {
        // Reset before reducing: the shader takes plain min/max, so the seed
        // values must be the identity for each.
        range.write(asWriteData(new Uint32Array([0xffffffff, 0])));
        reducer.set({ uniforms: { count: depthElementCount(model) }, depth, range });
        reducer.dispatch(1);
      }
 
      effect.set({
        uniforms: {
          resolution: output.size,
          depth_size: [model.width, model.height],
          mode: autoRange ? PRESENTATION_AUTO_RANGE : PRESENTATION_LOG_METRIC,
          near_meters:
            model.presentation.mode === 'log-metric' ? model.presentation.nearMeters : 0.35,
          far_meters: model.presentation.mode === 'log-metric' ? model.presentation.farMeters : 10,
          has_result: hasResult ? 1 : 0,
        },
        depth,
        range,
        colour,
      });
      runFrame(currentGpu, (frame) => frame.pass({ target: output }, (pass) => pass.draw(effect)));
    },
    dispose() {
      range.dispose();
    },
  };
}
 
/** vgpu-owned buffer sized for one depth result; used for idle and fixture frames. */
export function createDepthBuffer(gpu: Gpu, model: DepthModel, label = 'depth-estimation'): Buffer {
  return gpu.device.createBuffer({
    size: depthByteLength(model),
    usage: ['storage', 'copy_dst'],
    label: `${label}-depth`,
  });
}
 
export function writeDepth(buffer: Buffer, values: Float32Array): void {
  buffer.write(asWriteData(values));
}
 
/** vgpu-owned buffer holding one RGBA8 frame for the colour half. */
export function createColourBuffer(gpu: Gpu, model: DepthModel, label = 'depth-estimation'): Buffer {
  return gpu.device.createBuffer({
    size: model.width * model.height * 4,
    usage: ['storage', 'copy_dst'],
    label: `${label}-colour`,
  });
}
 
export function writeColour(buffer: Buffer, rgba: Uint8ClampedArray): void {
  buffer.write(asWriteData(rgba));
}