Implementing a 3D Automotive Radar Visualization with three.js
This article walks through building a 3D automotive radar effect using three.js, covering background image loading, fog integration, car model import, radar sector creation with CircleGeometry and Line2, dynamic tween animations, and road texture scrolling to simulate vehicle motion.
The article introduces a 3D automotive radar visualization project built with three.js, Node.js, and Vite, providing the source repository link.
Inspiration
Images of the desired effect are shown to illustrate the final appearance.
Technology Stack
three.js 0.157.0
nodejs v18.19.0
vite 4.3.2
Implementation Overview
Background Image
A custom background is created using THREE.TextureLoader and applied to a THREE.MeshPhongMaterial . The texture is repeated and added to the scene as a ground plane.
const createGround = () => {
const textureLoader = new THREE.TextureLoader();
const material = new THREE.MeshPhongMaterial({
color: 0xFFFFFF,
transparent: true,
opacity: 1
});
ground = new THREE.Mesh(new THREE.BoxGeometry(18, 0, 640, 1, 1, 1), material);
textureLoader.load(`${import.meta.env.VITE_ASSETS_URL}/assets/images/背景图.png`, function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(1, 12);
ground.material.map = texture;
ground.material.needsUpdate = true;
ground.rotation.copy(new THREE.Euler(-Math.PI, -Math.PI * 0.5, 0));
scene.add(ground);
});
};Fog is added to the scene via scene.fog to soften distant objects.
Fog Explanation
The Fog class defines atmospheric fog that affects all objects in the scene.
Loading the Car Model
const gltf = await loadGltf(`${import.meta.env.VITE_ASSETS_URL}/assets/models/car/scene.gltf`);
const model = gltf.scene;
const playerScale = 0.6;
model.scale.set(playerScale, playerScale, playerScale);
playerGroup.add(model);
scene.add(playerGroup);Radar Wave Creation
The radar effect is a semi‑transparent sector built with THREE.CircleGeometry and a surrounding line created from Line2 . The method accepts radius and thetaLength parameters.
const createRadar = (radius: number, thetaLength: number) => {
const geometry = new THREE.CircleGeometry(radius, 361, -thetaLength / 2, thetaLength);
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.5,
});
const circle = new THREE.Mesh(geometry, material);
circle.rotation.set(Math.PI * 0.5, 0, 0);
const position = geometry.getAttribute('position');
const { count } = position;
const linePoints = [];
for (let i = 1; i < count; i++) {
const v3 = new THREE.Vector3().fromBufferAttribute(position, i);
linePoints.push(...v3.toArray());
}
const line2Geometry = new LineGeometry();
line2Geometry.setPositions(linePoints);
const matLine = new LineMaterial({
linewidth: 0.002,
dashed: true,
opacity: 1,
color: 0xffffff,
vertexColors: false,
});
const line2 = new Line2(line2Geometry, matLine);
circle.add(line2);
scene.add(circle);
};Radar Diffusion Animation
The radar sector is animated by tweening radius and/or thetaLength using TWEEN.Tween . Different animation types ("radius", "thetaLength", or both) are supported.
const createRadar = (radius: number, thetaLength: number, index?: number, type?: "radius" | "thetaLength") => {
const InitialThetaLength = Math.PI * 0.01;
const InitialRadiue = 0.001;
const tweenTime = (type === 'radius' ? 2 : thetaLength) * 1000;
new TWEEN.Tween({ radius: InitialRadiue, thetaLength: InitialThetaLength })
.to({ radius, thetaLength }, tweenTime)
.delay(1000 * 5 - tweenTime)
.start()
.onUpdate(({ radius: r, thetaLength: t }) => {
let newGeometry: THREE.CircleGeometry;
if (type === 'radius') {
newGeometry = new THREE.CircleGeometry(r, 361, -thetaLength / 2, thetaLength);
} else if (type === 'thetaLength') {
newGeometry = new THREE.CircleGeometry(radius, 361, -t / 2, t);
} else {
newGeometry = new THREE.CircleGeometry(r, 361, -t / 2, t);
}
circle.geometry.setAttribute('position', newGeometry.getAttribute('position'));
const linePoints = geometryAttribute2Array(newGeometry);
line2Geometry.setPositions(linePoints);
})
.onComplete(() => {});
};Other Vehicles
Multiple additional car models are cloned, positioned on predefined lane offsets, and given a yoyo tween for slight back‑and‑forth motion.
let ZP = [0, -1.8, -3.8, 4.4, 6.3, 8.2];
for (let i = 0; i < 10; i++) {
const OC = otherCarModel.clone();
const x = getRandomIntegerInRange(-10, 0);
const z = ZP[getRandomIntegerInRange(0, ZP.length - 1)];
if (z <= 0) {
OC.rotation.set(0, -Math.PI * 0.5, 0);
}
const group = new THREE.Group();
group.add(OC);
group.position.copy(new THREE.Vector3(x - i * size.x, 0, z));
yoyoTweene(OC);
otherCarGroup.add(group);
}Vehicle Motion
Each vehicle receives a yoyo tween that moves it along the X axis to simulate uneven speed.
const yoyoTweene = (mesh: THREE.Object3D) => {
const x = getRandomIntegerInRange(-3, 3);
return new TWEEN.Tween({ x })
.to({ x: 3 }, 3000)
.yoyo(true)
.repeat(Infinity)
.start()
.onUpdate(({ x }) => {
if (mesh) mesh.position.setX(x);
});
};Ground Texture Scrolling
By adjusting the texture offset over time, the road appears to move backward, creating the illusion of forward vehicle motion.
const loopGround = () => {
new TWEEN.Tween({ offsetX: 0.12 })
.to({ offsetX: 1.12 }, 3000)
.repeat(Infinity)
.onUpdate(({ offsetX }) => {
ground.material.map.offset.set(0, offsetX);
})
.start();
};The article concludes with a note that the opposite‑direction lane vehicles appear to move backward, inviting readers to experiment with solutions.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.