// Overlap query probe — a Meep example for PhysicsSystem.overlap.
//
// A semi-transparent capsule follows the cursor and slowly spins. Every frame
// it asks the physics world a single question — "which bodies do I overlap,
// right now, at this position and orientation?" — and recolours the answer.
// Nothing is simulated against the probe; `overlap` is a read-only query, so
// the capsule glides through the field like a flashlight beam, not a bulldozer.
//
// The capsule is deliberately horizontal and always turning: the overlap test
// runs against the shape at its true world orientation, so this is also a
// demonstration that the query handles arbitrary rotations, not just
// axis-aligned boxes.
//
// Sections:
// §1 Tuning constants
// §2 Engine bootstrap physics + rendering systems, fixed camera
// §3 The ground one Static squashed box
// §4 The body field a grid of Dynamic bodies at rest, each with
// its own material so we can recolour it
// §5 The probe a non-physical capsule: a mesh + a query shape
// §6 Per-frame: move, spin, overlap, recolour
// §7 HUD
import * as THREE from "three";
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { Transform } from "@woosh/meep-engine/src/engine/ecs/transform/Transform.js";
import { ShadedGeometry } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js";
import { ShadedGeometryFlags } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometryFlags.js";
import { CapsuleGeometry } from "@woosh/meep-engine/src/engine/graphics/geometry/CapsuleGeometry.js";
import { PhysicsSystem } from "@woosh/meep-engine/src/engine/physics/ecs/PhysicsSystem.js";
import { ColliderObserverSystem } from "@woosh/meep-engine/src/engine/physics/ecs/ColliderObserverSystem.js";
import { RigidBody } from "@woosh/meep-engine/src/engine/physics/ecs/RigidBody.js";
import { Collider } from "@woosh/meep-engine/src/engine/physics/ecs/Collider.js";
import { BodyKind } from "@woosh/meep-engine/src/engine/physics/ecs/BodyKind.js";
import { BoxShape3D } from "@woosh/meep-engine/src/core/geom/3d/shape/BoxShape3D.js";
import { CapsuleShape3D } from "@woosh/meep-engine/src/core/geom/3d/shape/CapsuleShape3D.js";
import { SphereShape3D } from "@woosh/meep-engine/src/core/geom/3d/shape/SphereShape3D.js";
import Vector2 from "@woosh/meep-engine/src/core/geom/Vector2.js";
import Vector3 from "@woosh/meep-engine/src/core/geom/Vector3.js";
import { randomFloatBetween } from "@woosh/meep-engine/src/core/math/random/randomFloatBetween.js";
import {
plane3_compute_ray_intersection
} from "@woosh/meep-engine/src/core/geom/3d/plane/plane3_compute_ray_intersection.js";
// ─── §1 Tuning constants ────────────────────────────────────────────────────
// Ground: one squashed box. Its top face sits at y = 0.
const GROUND_WIDTH = 22;
const GROUND_HEIGHT = 1.5;
const GROUND_DEPTH = 22;
// Body field: a COLS × COLS grid spanning ±FIELD_HALF on the platform.
const FIELD_COLS = 7;
const FIELD_HALF = 8;
// The probe capsule. Radius is generous so the (horizontal) capsule spans the
// full height of the resting bodies — overlap then reads as a clean 2D sweep,
// driven by the capsule's position and spin.
const PROBE_RADIUS = 1.0;
const PROBE_LENGTH = 4.0; // cylinder section, excluding the caps
const PROBE_Y = 1.1; // height of the probe's centre above the ground
const PROBE_SPIN_RATE = 0.6; // rad/s about the vertical axis
const PROBE_CLAMP = 11; // keep the probe over (and just past) the platform
const HIGHLIGHT_COLOR = 0x4ef0a8; // overlapping → meep green
const HIGHLIGHT_EMISSIVE = 0x1a5c3c; // a little glow so it really pops
// Muted base palette for the field — chosen to sit back so the highlight reads.
const BASE_COLORS = [0x3b4759, 0x45526a, 0x515f74, 0x5b6b80, 0x6a5d7a, 0x4a6470];
// ─── §2 Engine bootstrap ────────────────────────────────────────────────────
const engine = await EngineHarness.bootstrap({
configuration: (config, engine) => {
config.addSystem(new ShadedGeometrySystem(engine));
const physics = new PhysicsSystem();
config.addSystem(physics);
config.addSystem(new ColliderObserverSystem(physics));
},
});
// `cameraController: false` — the camera is fixed. We never wire orbit / pan
// input, so the only thing the user moves is the cursor (and thus the probe).
await EngineHarness.buildBasics({
engine,
enableTerrain: false,
enableWater: false,
enableLights: true,
enableShadows: true,
shadowmapResolution: 2048,
focus: new Vector3(0, 0.5, 0),
distance: 30,
pitch: 0.95,
yaw: 0.5,
cameraFarDistance: 200,
cameraController: false,
showFps: false,
});
const ecd = engine.entityManager.dataset;
const physics = engine.entityManager.getSystem(PhysicsSystem);
// ─── §3 The ground ──────────────────────────────────────────────────────────
let groundEntity;
{
const transform = new Transform();
transform.position.set(0, -GROUND_HEIGHT / 2, 0); // top face at y = 0
const body = new RigidBody();
body.kind = BodyKind.Static;
const collider = new Collider();
collider.shape = BoxShape3D.from_size(GROUND_WIDTH, GROUND_HEIGHT, GROUND_DEPTH);
collider.friction = 0.8;
const geometry = new THREE.BoxGeometry(GROUND_WIDTH, GROUND_HEIGHT, GROUND_DEPTH);
const material = new THREE.MeshStandardMaterial({ color: 0x222a33, roughness: 0.95, metalness: 0 });
groundEntity = new Entity()
.add(transform)
.add(body)
.add(collider)
.add(ShadedGeometry.from(geometry, material))
.build(ecd);
}
// ─── §4 The body field ──────────────────────────────────────────────────────
//
// A tidy grid of Dynamic bodies placed exactly at rest on the platform. They
// settle (they're already touching the ground) and sleep immediately; the probe
// never disturbs them because an overlap query applies no impulse.
//
// The crucial difference from the rain example: each body gets its OWN material
// instance. ShadedGeometrySystem reads a body's material live every frame, so
// mutating `material.color` recolours exactly one body — which is how we paint
// the overlap result. (Sharing a material would batch the bodies and recolour
// them as a group.)
const Y_AXIS = new THREE.Vector3(0, 1, 0);
const Z_AXIS = new THREE.Vector3(0, 0, 1);
const scratchQuat = new THREE.Quaternion();
// entity → { material, baseHex } so we can paint and restore it.
const fieldBodies = new Map();
// Three body archetypes. Each returns { geometry, shape, restY, lieDown };
// `restY` is the centre height that leaves the body sitting on y = 0.
function makeArchetype() {
const roll = Math.random();
if (roll < 0.34) {
// Sphere — collider radius matches the mesh.
return {
geometry: new THREE.SphereGeometry(1, 20, 14),
shape: SphereShape3D.from(1),
restY: 1,
lieDown: false,
};
} else if (roll < 0.67) {
// Box of varied size.
const w = randomFloatBetween(Math.random, 1.2, 2.0);
const h = randomFloatBetween(Math.random, 1.0, 1.8);
const d = randomFloatBetween(Math.random, 1.2, 2.0);
return {
geometry: new THREE.BoxGeometry(w, h, d),
shape: BoxShape3D.from_size(w, h, d),
restY: h / 2,
lieDown: false,
};
} else {
// Capsule, laid on its side so the field has non-axis-aligned bodies too.
const cr = randomFloatBetween(Math.random, 0.5, 0.7);
const ch = randomFloatBetween(Math.random, 0.8, 1.6);
return {
geometry: new CapsuleGeometry(cr, ch, 8, 16),
shape: CapsuleShape3D.from(cr, ch),
restY: cr,
lieDown: true,
};
}
}
let colorCursor = 0;
const step = (2 * FIELD_HALF) / (FIELD_COLS - 1);
for (let ix = 0; ix < FIELD_COLS; ix++) {
for (let iz = 0; iz < FIELD_COLS; iz++) {
const a = makeArchetype();
const x = -FIELD_HALF + ix * step + randomFloatBetween(Math.random, -0.3, 0.3);
const z = -FIELD_HALF + iz * step + randomFloatBetween(Math.random, -0.3, 0.3);
const transform = new Transform();
transform.position.set(x, a.restY, z);
// A random yaw, plus a 90° tilt for capsules so they lie horizontally.
const yaw = randomFloatBetween(Math.random, 0, Math.PI * 2);
scratchQuat.setFromAxisAngle(Y_AXIS, yaw);
if (a.lieDown) {
scratchQuat.multiply(new THREE.Quaternion().setFromAxisAngle(Z_AXIS, Math.PI / 2));
}
transform.rotation.set(scratchQuat.x, scratchQuat.y, scratchQuat.z, scratchQuat.w);
const body = new RigidBody();
body.kind = BodyKind.Dynamic;
body.mass = 1;
// Rotation stays locked (default inverse inertia of 0): the bodies are a
// still target for the probe, so they keep the orientation we gave them.
const collider = new Collider();
collider.shape = a.shape;
collider.friction = 0.8;
const baseHex = BASE_COLORS[colorCursor++ % BASE_COLORS.length];
const material = new THREE.MeshStandardMaterial({ color: baseHex, roughness: 0.7, metalness: 0.05 });
const entity = new Entity()
.add(transform)
.add(body)
.add(collider)
.add(ShadedGeometry.from(a.geometry, material))
.build(ecd);
fieldBodies.set(entity, { material, baseHex });
}
}
// ─── §5 The probe ───────────────────────────────────────────────────────────
//
// The probe is NOT a physics body — it carries no RigidBody and no Collider, so
// it never enters the simulation or the broadphase. It is two things:
//
// 1. a semi-transparent capsule mesh (Transform + ShadedGeometry) we move and
// spin for the user to see, and
// 2. a CapsuleShape3D of identical dimensions that we hand to `overlap`.
//
// They share one world pose, so what you see is exactly what gets queried.
const probeShape = CapsuleShape3D.from(PROBE_RADIUS, PROBE_LENGTH);
const probeTransform = new Transform();
probeTransform.position.set(0, PROBE_Y, 0);
{
const geometry = new CapsuleGeometry(PROBE_RADIUS, PROBE_LENGTH, 10, 22);
const material = new THREE.MeshStandardMaterial({
color: HIGHLIGHT_COLOR,
transparent: true,
opacity: 0.4,
depthWrite: false, // a ghost — don't let it occlude the bodies behind it
roughness: 0.4,
});
const sg = ShadedGeometry.from(geometry, material);
sg.clearFlag(ShadedGeometryFlags.CastShadow); // the probe casts no shadow
new Entity()
.add(probeTransform)
.add(sg)
.build(ecd);
}
// ─── §6 Per-frame: move, spin, overlap, recolour ────────────────────────────
// Scratch + state reused every frame (zero per-frame allocation).
const ndc = new Vector2();
const rayOrigin = new Vector3();
const rayDirection = new Vector3();
const planeHit = new Vector3();
const tiltQuat = new THREE.Quaternion().setFromAxisAngle(Z_AXIS, Math.PI / 2); // Y-axis capsule → horizontal
const spinQuat = new THREE.Quaternion();
const probeQuat = new THREE.Quaternion();
const probeRotation = { x: 0, y: 0, z: 0, w: 1 }; // plain object for overlap()
const overlapIds = new Uint32Array(64);
// Overlap state, double-buffered so we only touch materials on a transition.
let highlighted = new Set();
let nextHighlighted = new Set();
// Skip the ground (it's a body too, and the probe rides just above it).
const overlapFilter = (entity) => entity !== groundEntity;
let spinAngle = 0;
let lastFrameMs = performance.now();
let fpsWindow = 0, fpsFrames = 0;
const fpsEl = document.getElementById("fps");
const countEl = document.getElementById("count");
engine.graphics.on.postRender.add(() => {
const nowMs = performance.now();
const dt = (nowMs - lastFrameMs) / 1000;
lastFrameMs = nowMs;
// — Orientation: horizontal capsule, spun about the vertical axis —
spinAngle += PROBE_SPIN_RATE * dt;
spinQuat.setFromAxisAngle(Y_AXIS, spinAngle);
probeQuat.copy(spinQuat).multiply(tiltQuat);
probeRotation.x = probeQuat.x;
probeRotation.y = probeQuat.y;
probeRotation.z = probeQuat.z;
probeRotation.w = probeQuat.w;
probeTransform.rotation.set(probeQuat.x, probeQuat.y, probeQuat.z, probeQuat.w);
// — Position: cursor ray → horizontal plane at PROBE_Y —
// engine.devices.pointer already tracks the live cursor position for us;
// normalize it to clip space and project it into the world.
engine.graphics.normalizeViewportPoint(engine.devices.pointer.position, ndc);
engine.graphics.viewportProjectionRay(ndc.x, ndc.y, rayOrigin, rayDirection);
const hit = plane3_compute_ray_intersection(
planeHit,
rayOrigin.x, rayOrigin.y, rayOrigin.z,
rayDirection.x, rayDirection.y, rayDirection.z,
0, 1, 0, -PROBE_Y, // plane y = PROBE_Y (n·x + dist = 0)
);
if (hit) {
const px = Math.max(-PROBE_CLAMP, Math.min(PROBE_CLAMP, planeHit.x));
const pz = Math.max(-PROBE_CLAMP, Math.min(PROBE_CLAMP, planeHit.z));
probeTransform.position.set(px, PROBE_Y, pz);
}
// — The query —
const p = probeTransform.position;
const count = physics.overlap(
probeShape,
{ x: p.x, y: p.y, z: p.z },
probeRotation,
overlapIds, 0,
overlapFilter,
);
// — Recolour: paint the new set, restore bodies that left it —
nextHighlighted.clear();
for (let i = 0; i < count; i++) {
const entity = physics.entityOf(overlapIds[i]);
const record = fieldBodies.get(entity);
if (record === undefined) continue; // ground / probe / stale id
nextHighlighted.add(entity);
if (!highlighted.has(entity)) {
record.material.color.setHex(HIGHLIGHT_COLOR);
record.material.emissive.setHex(HIGHLIGHT_EMISSIVE);
}
}
for (const entity of highlighted) {
if (!nextHighlighted.has(entity)) {
const record = fieldBodies.get(entity);
if (record !== undefined) {
record.material.color.setHex(record.baseHex);
record.material.emissive.setHex(0x000000);
}
}
}
const tmp = highlighted;
highlighted = nextHighlighted;
nextHighlighted = tmp;
// — HUD —
if (countEl) countEl.textContent = String(highlighted.size);
fpsWindow += dt;
fpsFrames++;
if (fpsWindow >= 0.5) {
if (fpsEl) fpsEl.textContent = (fpsFrames / fpsWindow).toFixed(0);
fpsWindow = 0;
fpsFrames = 0;
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Overlap query probe · Meep</title>
<meta name="robots" content="noindex">
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
width: 100%; height: 100%;
overflow: hidden;
background: #07090c;
color: #e6edf3;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
cursor: crosshair;
}
.panel {
position: fixed;
z-index: 100;
background: rgba(7, 9, 12, 0.72);
border: 1px solid #1f2731;
border-radius: 10px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 12px 32px rgba(0,0,0,0.4);
}
.hud {
top: 1rem; left: 1rem;
padding: 0.8rem 1rem;
font-family: ui-monospace, "JetBrains Mono", monospace;
font-size: 0.82rem;
line-height: 1.7;
color: #9aa5b1;
min-width: 180px;
}
.hud .label {
color: #6b7785;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.65rem;
margin-right: 0.5rem;
}
.hud .value { color: #4ef0a8; }
.legend {
bottom: 1rem; left: 1rem;
padding: 0.8rem 1rem;
font-size: 0.82rem;
line-height: 1.55;
max-width: 420px;
color: #9aa5b1;
}
.legend strong { color: #e6edf3; }
</style>
</head>
<body>
<div class="panel hud">
<div><span class="label">fps</span><span class="value" id="fps">--</span></div>
<div><span class="label">overlapping</span><span class="value" id="count">0</span></div>
</div>
<div class="panel legend">
<strong>Move your cursor.</strong> A semi-transparent capsule follows it and
slowly spins, running <code>PhysicsSystem.overlap</code> every frame — every
body it intersects, at its true (non-axis-aligned) orientation, lights up.
The camera is fixed; the probe is a query, not a collider, so it passes
through without pushing anything.
</div>
<script type="module" src="./src/main.js"></script>
</body>
</html>
{
"title": "Overlap query probe",
"description": "A cursor-driven, slowly-spinning capsule runs PhysicsSystem.overlap every frame; bodies it intersects light up — arbitrary orientation, no collision.",
"category": "Physics",
"status": "live",
"order": 2,
"tags": ["physics", "overlap", "query", "collision", "ecs"],
"sourceHint": "examples-src/overlap-query/",
"demoUrl": "/examples/overlap-query/demo.html",
"defaultFile": "src/main.js"
}
{
"name": "@meep-examples/overlap-query",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "A cursor-driven, spinning capsule runs PhysicsSystem.overlap every frame — bodies it intersects change colour.",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@woosh/meep-engine": "2.158.0",
"three": "0.136.0"
},
"devDependencies": {
"@rollup/plugin-strip": "^3.0.4",
"vite": "^8.0.13"
}
}
import { defineConfig } from "vite";
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
import strip from "@rollup/plugin-strip";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
plugins: [
{
// Copy the committed source thumbnail into the generated gallery folder.
// public/examples/<id>/ is build output (gitignored); thumbnail.png is
// kept in source here and copied through on every build so the gallery
// (src/data/examples.ts) can resolve it.
name: "copy-thumbnail",
apply: "build",
closeBundle() {
const thumb = resolve(__dirname, "thumbnail.png");
const dst = resolve(__dirname, "../../public/examples/overlap-query");
if (existsSync(thumb)) {
mkdirSync(dst, { recursive: true });
copyFileSync(thumb, resolve(dst, "thumbnail.png"));
}
},
},
],
base: "./",
build: {
outDir: resolve(__dirname, "../../public/examples/overlap-query"),
emptyOutDir: false,
rollupOptions: {
input: resolve(__dirname, "demo.html"),
plugins: [
{
// this will remove all assert statements from the production build
...strip(),
apply: 'build'
}
],
},
target: "es2022",
},
});