Skip to main content

Radar Sweep

Type​

Class

A ground-radar pulse: a ring band rolls out from the mesh origin as progress goes from 0 to 1, broken up by drifting noise so the crest never reads as a drawn circle. Each pulse paints a leading edge, a weaker first echo trailing it, the faint already-swept footprint and short blips over buried features.

When a heightAt sampler is supplied the band is displaced onto the terrain, so the pulse follows uneven ground instead of slicing through it. Set the mesh position before adding it to the scene (the band is rebuilt on added), or call rebuild() after moving it.

Pass period to let the pulse loop on its own — no animation loop is needed. Omit period and write progress every frame to drive the scan manually.

Default Usage​

Every example below can be viewed with either the WebGPU (default) or the WebGL renderer — switch tabs to compare.

import { useRef, useEffect } from 'react'
import * as THREE from 'three'
import { Scene, RadarSweep } from 'react-three-lite'
import type { SceneComponents } from 'react-three-lite'

function terrainHeight(x: number, z: number): number {
return Math.sin(x * 0.55) * 0.22 + Math.cos(z * 0.4) * 0.18
}

export default function App() {
const sweepRef = useRef<RadarSweep | null>(null)
const groundRef = useRef<THREE.Mesh | null>(null)

const handleCreated = (scene: THREE.Scene, components: SceneComponents) => {
const { camera } = components
camera.position.set(0, 7, 10)
camera.lookAt(0, 0, 0)

// Displace the ground with the same sampler the sweep conforms to.
const groundGeometry = new THREE.PlaneGeometry(22, 22, 96, 96)
groundGeometry.rotateX(-Math.PI / 2)
const positions = groundGeometry.attributes.position as THREE.BufferAttribute
for (let i = 0; i < positions.count; i += 1) {
positions.setY(i, terrainHeight(positions.getX(i), positions.getZ(i)))
}
groundGeometry.computeVertexNormals()

const ground = new THREE.Mesh(
groundGeometry,
new THREE.MeshStandardMaterial({ color: 0x4a3a33, roughness: 1 })
)
scene.add(ground)
groundRef.current = ground

// `period` makes the sweep loop on its own; no animation loop required.
const sweep = new RadarSweep({ radius: 8, heightAt: terrainHeight, period: 2.4 })
scene.add(sweep)
sweepRef.current = sweep
}

// Cleanup on unmount
useEffect(() => {
return () => {
sweepRef.current?.dispose()
sweepRef.current = null

const ground = groundRef.current
if (ground) {
ground.parent?.remove(ground)
ground.geometry.dispose()
;(ground.material as THREE.Material).dispose()
}
groundRef.current = null
}
}, [])

return (
<Scene
bgColor="rgb(40, 42, 54)"
onCreated={handleCreated}
style={{ marginTop: '10px', width: '100%', height: '300px' }}
/>
)
}

Options​

PropertyTypeDefaultDescription
radiusnumber8Outer radius the pulse expands to
segmentsnumber128Radial segment count of the ring
ringsnumber20Concentric segment count of the ring
heightAt(x: number, z: number) => number—World-space ground sampler. When given, the band is displaced onto the terrain
colornumber0x57ccffColor of the sweeping bands
crestColornumber0xa8f5ffColor of the leading crest
echoDelaynumber1.8Distance the weaker first return trails behind the crest
opacitynumber0.9Alpha multiplier of the whole pulse
liftnumber0.04Height offset above the terrain
progressnumber0Initial pulse progress in the 0-1 range
periodnumber—Seconds per full sweep. When set, the pulse advances on its own from the render timer and no per-frame code is needed

Properties​

NameTypeDescription
progressnumberPulse progress. 0 starts at the origin, 1 reaches the outer radius. With period set, writing it shifts the phase and the pulse keeps looping from there; without it, write it every frame to drive the scan

Methods​

NameParametersDescription
constructor(options?: RadarSweepOptions)Create the pulse. It extends THREE.Mesh, so position it and add it to the scene
rebuild() => voidRe-sample heightAt for the current world position. Called automatically on added
dispose() => voidRemove the added listener, dispose the geometry and material and detach from the scene