World – Hot Air Balloon

The Art of the Drift

Engineering a Relaxing Hot Air Balloon System in Three.js

Date: September 18, 2026
Project: World Simulation
Focus: Vehicle Physics, Camera Immersion, and Collision Resolution


1. Abstract

The objective was to implement a Hot Air Balloon vehicle that felt “weightless” and “atmospheric.” Initial attempts resulted in “snappy” movement akin to a ground vehicle. Through iterative testing, we moved from Kinematic Movement (instant) to Momentum-Based Physics (drifting), while solving critical bugs regarding spawn-teleportation and collision locking.


2. The Evolution: What Went Wrong & What Went Right

❌ The “Bad” (Initial Pain Points)

  • The “Car” Feel: Initially, the balloon stopped the millisecond the key was released. It felt like a floating car, not a balloon.
  • The “Jump” Bug: Upon entering the balloon, the code forced a +3 height increase. This looked like a “glitchy jump” and ruined the smooth entry.
  • The “Stuck” Paradox: When exiting in mid-air, the player was teleported instantly to the ground. If a building was beneath them, they spawned inside the mesh, triggering the isBlocked function and freezing the player in place.
  • Speed Issues: Initial momentum was too high, making the balloon feel like a jet engine rather than a peaceful drift.

✅ The “Good” (The Wins)

  • Momentum Flow: Implementing a velocity vector allowed the balloon to “glide.” Now, it takes time to speed up and time to slow down.
  • The Aerial Perspective: Switching to a 45-degree diagonal camera shifted the experience from “playing a game” to “viewing a landscape.”
  • Natural Exit: By removing the forced ground-snap, players now fall naturally from the balloon, adding a sense of gravity and danger.

3. Technical Implementation (The “Golden” Code)

To achieve the final feel, we modified two core systems: the Vehicle Logic (index.html) and the Object Classification (build.js).

A. The Physics Engine (index.html)

We replaced instant position updates with a Velocity-Friction Model.

The Constants:

code JavaScript

// Ultra-Slow Floating Physics
const BALLOON_ACCEL = 0.5;      // Slow start, prevents "snapping"
const BALLOON_FRICTION = 0.998; // Near-zero friction = infinite glide
const BALLOON_MAX_SPEED = 4.0;  // Capped for a relaxing pace
const BALLOON_LIFT_SPEED = 5.0; // Constant lift for the Q key

The Logic Loop:

code JavaScript

// Inside animate() -> if (isBalloon)
const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(activeControl.quaternion);

// Horizontal Momentum (W/S)
if (keys['w']) balloonVelocity.addScaledVector(forward, BALLOON_ACCEL * delta);
if (keys['s']) balloonVelocity.addScaledVector(forward, -BALLOON_ACCEL * delta);

// Direct Lift (Q) - No momentum, just constant rise
if (keys['q']) activeControl.position.y += BALLOON_LIFT_SPEED * delta;

// Downward Drift (E) - Uses momentum to "sink"
if (keys['e']) balloonVelocityY -= BALLOON_ACCEL * delta;

// Apply Glide Friction
balloonVelocity.multiplyScalar(BALLOON_FRICTION);
balloonVelocityY *= BALLOON_FRICTION;

// Final Position Update
activeControl.position.add(balloonVelocity);
activeControl.position.y += balloonVelocityY;

B. The Cinematic Camera (index.html)

To simulate “Sunlight at 9 AM,” we positioned the camera at a high diagonal offset.

code JavaScript

// Specialized Balloon Camera View
if (isDriving && currentVehicle && currentVehicle.userData && currentVehicle.userData.isBalloon) {
    const camDist = 25;
    // Offset camera 45 degrees top-down and behind
    const offset = new THREE.Vector3(camDist, camDist, camDist).applyQuaternion(activeControl.quaternion);
    _vLookTarget.copy(activeControl.position).add(offset);
    camera.position.lerp(_vLookTarget, delta * 5);
    camera.lookAt(activeControl.position);
}

C. The Build Classification (build.js)

For the physics to know it’s a balloon and not a car, we added metadata during the GLB load.

code JavaScript

// Inside createInScene(data)
const isBalloon = nameLower.includes('balloon') || nameLower.includes('hot-air');
if (isBalloon) {
    obj.userData.isVehicle = true;
    obj.userData.isBalloon = true; // This flag triggers the momentum physics
}

4. Conclusion & Final Tuning

The “Perfect Drift” was found by balancing Acceleration vs. Friction. By lowering acceleration to 0.5 and raising friction to 0.998, we achieved a state of “perpetual flow.”

The removal of the nearest.position.y += 3 line in toggleVehicle finally allowed the balloon to sit naturally at ground level 0, creating a seamless transition from walking to flying.

Current State: 🟢 Stable | Feeling: ☁️ Ethereal | Movement: 🌊 Fluid