b3d-radar
A radar sensor you attach to a platform (an aircraft, a turret). It enumerates the scene's radar-blips, detects those in range · profile and inside its cone, and acquires locks on the nearest opposed contacts, all via a pure, tested radar model (see The radar model, below). Nest it in the thing it rides:
b3dAircraft({ player: true, y: 0 },
b3dRadar({ range: 250, coneDeg: 90, lockTime: 1.5, maxLocks: 2 }))
Demo — watch a lock build and decay
The mast sweeps, so contacts enter and leave the cone on their own. Watch the
lock column: a contact inside the acquire cone climbs toward 1.00, holds while
it stays in the wider maintain cone, and drops the moment it falls out. The
grey contact is faction: 'neutral' — detected, never locked.
import { b3d, b3dRadar, b3dRadarBlip, b3dBox, label3d, slider3d } from 'tosijs-3d'
import { demoStage, orbitCam } from 'tosijs-3d/demo-utils'
import { tosi, elements } from 'tosijs'
const { div } = elements
const { rad } = tosi({ rad: { sweep: 35, range: 60 } })
// The platform: a mast the radar rides. A radar finds what it is nested in.
const radar = b3dRadar({ range: rad.range, coneDeg: 70, acquireConeDeg: 28, lockTime: 1.2, maxLocks: 2 })
const mast = b3dBox({ meshName: 'mast', width: 0.8, depth: 0.8, height: 3, y: 1.5, color: '#8a94a6' }, radar)
// Contacts: each a box carrying a blip, so the blip follows the mesh.
const contacts = [
{ name: 'alpha', faction: 'hostile', profile: 1.4, r: 22, speed: 0.35, color: '#d05050' },
{ name: 'bravo', faction: 'hostile', profile: 0.6, r: 34, speed: -0.22, color: '#d07a50' },
{ name: 'civil', faction: 'neutral', profile: 1.2, r: 27, speed: 0.5, color: '#9aa5b1' },
]
// Keep the BLIP elements: a track's `id` is the blip element itself, not a
// name, so the readout matches by identity.
const blips = contacts.map((c) => b3dRadarBlip({ faction: c.faction, profile: c.profile }))
const boxes = contacts.map((c, i) =>
b3dBox({ meshName: c.name, size: 1.2, y: 1, color: c.color, glow: 0.3 }, blips[i]))
const readout = div({ style: 'font:12px ui-monospace,monospace; padding:6px; white-space:pre' })
const scene = b3d(
{
glowLayerIntensity: 0.6,
panelTitle: 'radar',
scenePanel: () => [
slider3d({ label: 'sweep °/s', value: rad.sweep, min: 0, max: 90, step: 5 }),
slider3d({ label: 'range', value: rad.range, min: 15, max: 90, step: 5 }),
],
sceneCreated(el) {
orbitCam(el, { radius: 46, betaDeg: 56.2, target: [0, 1, 0] })
let t = 0
el.scene.registerBeforeRender(() => {
const dt = el.frameDelta
t += dt
// ry is DEGREES — the cone sweeps, so contacts come and go on their own.
mast.ry = (mast.ry + rad.sweep.valueOf() * dt) % 360
radar.range = rad.range.valueOf()
boxes.forEach((b, i) => {
const c = contacts[i]
b.x = Math.cos(t * c.speed + i * 2) * c.r
b.z = Math.sin(t * c.speed + i * 2) * c.r
})
const rows = (radar.tracks ?? []).map((tr) => {
const c = contacts[blips.indexOf(tr.id)] ?? contacts[0]
const bar = '█'.repeat(Math.round(tr.lockProgress * 10)).padEnd(10, '·')
return `${c.name.padEnd(6)} ${String(Math.round(tr.distance)).padStart(3)}m ` +
`${tr.detected ? 'seen' : ' '} ${bar} ${tr.locked ? 'LOCK' : ''}`
})
readout.textContent = rows.length
? 'contact dist det lock\n' + rows.join('\n')
: 'no contacts in cone'
})
},
},
...demoStage({ size: 90, tiles: 18, texture: '/tosi-warhol-testgrid.svg', timeOfDay: 10 }),
mast,
...boxes,
)
preview.append(scene, readout)
The platform reads radar.tracks (to plot the HUD) and radar.nearestLock (a homing
missile's target — no lock ⇒ the missile flies ballistic). A turret wants a cheap
one: maxLocks: 1, alignment: 'hostile' (so it only locks the player).
Attributes
| Attribute | Default | Description |
|---|---|---|
range |
250 |
Nominal range — a profile 1 blip is detected within this. |
coneDeg |
90 |
Detection cone HALF-angle (deg). 90 = front hemisphere. A lock is held while the contact stays inside it. |
acquireConeDeg |
60 |
Narrower half-angle (deg) a contact must be inside to START a lock. |
acquireRange |
0.5 |
Fraction of range a contact must be within to START a lock. Holding uses the full range. |
lockTime |
1.5 |
Seconds of continuous detection to acquire a lock (0 = instant). |
maxLocks |
2 |
Max simultaneous locks (the nearest opposed contacts). |
alignment |
'friendly' |
This radar's own faction. It locks only opposed blips (friendly⇄hostile); neutrals/waypoints show but never lock. |
updateInterval |
100 |
Radar refresh period (ms). Runs below frame-rate and is dithered across frames so many radars don't all recompute the same frame. |
The radar model
The sensing is a pure, deterministic Radar (plain {x, y, z}, time only via
dt), so it runs headless. A contact is detected within range · profile AND
inside the cone (the dot of the platform's forward with the bearing to the
contact is at least coneDot). Detected, lockable contacts build a lock over
lockTime seconds; up to maxLocks of the nearest hold locks at once, and
losing detection decays the lock back down. Step it yourself each tick:
import { Radar, coneDotFromDegrees, isOpposed } from 'tosijs-3d'
const radar = new Radar({
range: 200,
coneDot: coneDotFromDegrees(90), // hold a lock anywhere ahead
acquireConeDot: coneDotFromDegrees(30), // start one only near the nose
acquireRangeFraction: 0.5,
lockTime: 1.5,
maxLocks: 1,
})
const tracks = radar.update(
{ x: 0, y: 10, z: 0 }, // where the platform is
{ x: 0, y: 0, z: 1 }, // its nose, unit length
[{ id: 'bandit', pos: { x: 5, y: 12, z: 60 }, profile: 1, lockable: isOpposed('friendly', 'hostile') }],
dt
) // nearest first: { id, pos, distance, detected, lockProgress, locked }
radar.nearestLock // the missile's target, or null