Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/JustADev1024/threejs-car/llms.txt

Use this file to discover all available pages before exploring further.

Dream Car lets you switch two independent visual effects on and off at any time: a pair of amber headlights cast by a SpotLight and a PointLight attached to the car’s front, and a drifting exhaust cloud simulated by a THREE.Points particle system mounted at the car’s rear. Both are toggled with dedicated buttons in the control panel and are fully independent — you can run exhaust without lights, lights without exhaust, or both together.

Headlights

Light objects

Two Three.js lights are created at the top of the script and attached directly to the car Group, so they move in lockstep with the vehicle:
// Cone-shaped beam
const headlightSpot = new THREE.SpotLight(0xffaa66, 0);
headlightSpot.angle = 0.6;       // beam half-angle in radians (~34°)
headlightSpot.penumbra = 0.2;    // soft edge (20% of beam width)
headlightSpot.distance = 20;     // max illumination range (world units)
headlightSpot.castShadow = true;

// Soft ambient glow around the lamp housing
const headlightPoint = new THREE.PointLight(0xffaa66, 0, 15);
headlightPoint.castShadow = true;
Both lights start with intensity = 0; they are activated at startup by the initial state logic (see below).

Lamp housing mesh

A small glowing sphere is placed at the car’s front to represent the physical headlight lens:
const headlightModel = new THREE.Mesh(
    new THREE.SphereGeometry(0.22, 32, 32),
    new THREE.MeshStandardMaterial({
        color: 0xffaa66,
        emissive: 0xff4422,
        emissiveIntensity: 0      // set to 1.2 when ON
    })
);
headlightModel.position.set(0, 0.25, 1.15);  // front-centre of car body
car.add(headlightModel);
The emissive channel makes the sphere appear self-lit when the headlights are on, without requiring an additional light source aimed at it.

Positioning on the car

car.add(headlightSpot);
headlightSpot.position.set(0, 0.3, 1.3);   // just ahead of the lamp housing

car.add(headlightPoint);
headlightPoint.position.set(0, 0.3, 1.2);  // coincides roughly with lamp housing
Because both lights are children of the car Group, their world-space positions update automatically whenever the car moves.

Toggle logic

lightsBtn.addEventListener('click', () => {
    lightsOn = !lightsOn;
    headlightModel.material.emissiveIntensity = lightsOn ? 1.2 : 0;
    headlightSpot.intensity                   = lightsOn ? 1.5 : 0;
    headlightPoint.intensity                  = lightsOn ? 0.8 : 0;
    updateLightsButton();
});

// Headlights are ON by default at startup
headlightModel.material.emissiveIntensity = 1.2;
headlightSpot.intensity = 1.5;
headlightPoint.intensity = 0.8;
StateheadlightSpot.intensityheadlightPoint.intensityemissiveIntensity
ON (default)1.50.81.2
OFF000
Setting all three to zero rather than removing the lights from the scene means toggling them back on is instant — no object creation or scene mutation is required.

Exhaust Particles

Particle system setup

The exhaust effect is a THREE.Points object — a cloud of small dots rendered with a single draw call. It is added as a child of the car Group so it inherits the car’s position automatically:
const particleCount = 200;
const particleGeo = new THREE.BufferGeometry();
const posArray = new Float32Array(particleCount * 3);  // x,y,z per particle
particleGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));

const particleMat = new THREE.PointsMaterial({
    color: 0xccccaa,       // warm grey-white (exhaust smoke)
    size: 0.08,
    transparent: true
});

const particles = new THREE.Points(particleGeo, particleMat);
car.add(particles);

updateExhaust() — the per-frame simulation loop

updateExhaust runs every animation frame via requestAnimationFrame. When exhaust is disabled it hides the mesh and returns early, keeping overhead near zero:
function updateExhaust() {
    if (!exhaustOnState) {
        particles.visible = false;
        requestAnimationFrame(updateExhaust);
        return;
    }
    particles.visible = true;
    const positions = particles.geometry.attributes.position.array;

    for (let i = 0; i < particleCount; i++) {
        if (Math.random() < 0.3) {
            // 30% chance: reset particle to the exhaust pipe origin
            positions[i*3]   = (Math.random() - 0.5) * 0.4;   // small X spread
            positions[i*3+1] = Math.random() * 0.3;             // slight upward spread
            positions[i*3+2] = -1.2 - Math.random() * 0.5;     // behind the car
        } else {
            // 70% chance: drift the particle further backward along Z
            positions[i*3+2] -= 0.07;
            // respawn if it has drifted too far
            if (positions[i*3+2] < -2.5) {
                positions[i*3]   = (Math.random() - 0.5) * 0.4;
                positions[i*3+1] = Math.random() * 0.3;
                positions[i*3+2] = -1.2;
            }
        }
    }
    particles.geometry.attributes.position.needsUpdate = true;
    requestAnimationFrame(updateExhaust);
}
updateExhaust(); // kicked off once at startup
Each frame, every particle independently rolls a 30/70 die:
  • 30% — reset to origin: The particle jumps back to the exhaust pipe area (z ≈ -1.2, scattered slightly in X and Y). This constantly replenishes fresh “smoke” near the car.
  • 70% — drift backward: The particle’s Z coordinate decreases by 0.07 units, simulating the cloud blowing behind the moving car. When a particle drifts past z = -2.5 it is recycled back to the origin.
Setting needsUpdate = true on the position attribute tells Three.js to re-upload the buffer to the GPU on the next render call.

Toggle logic

// exhaust is OFF by default
exhaustBtn.addEventListener('click', () => {
    exhaustOn = !exhaustOn;
    exhaustOnState = exhaustOn;  // exhaustOnState is read inside updateExhaust()
    updateExhaustButton();
});
The exhaustOnState variable is the flag read inside updateExhaust. Because the loop always keeps running (it calls requestAnimationFrame even when off), toggling the effect on is immediate — there is no loop to restart.
The button labels for both headlights and exhaust change automatically when you switch between English and Russian using the language buttons. In English the labels read ”💡 Headlights On / Off” and ”💨 Exhaust On / Off”; in Russian they switch to ”💡 Фары Вкл / Выкл” and ”💨 Выхлоп Вкл / Выкл”. This is handled by the updateLightsButton() and updateExhaustButton() helpers, which read the current language from the translations object via t('lightsOn'), t('lightsOff'), etc.

Build docs developers (and LLMs) love