3D Airplane Demo with React and Three.js

Codrops Article

Loading...

This is a cool demo I first saw on Codrops blogs many many years ago, thought it would be fun to implement it as a react nextjs component. The demo features a stylized airplane flying above a rotating sea, surrounded by drifting clouds in a dynamic sky environment.

components/PlaneDemo
├── classes
│   ├── Airplane.js
│   ├── Cloud.js
│   ├── Sea.js
│   └── Sky.js
├── constants
│   └── colors.js
├── index.jsx
└── utils
    └── scene.js

Original Demo

The original Codrops demo creates a 3D scene using Three.js with several key components:

  • A low-poly airplane that responds to mouse/touch input
  • An animated sea with waves
  • A sky populated with drifting clouds
  • Atmospheric lighting with a hemisphere light for ambient illumination and directional light for shadows

The scene is rendered in a continuous loop that:

  1. Updates the sea's waves by rotating its mesh
  2. Moves clouds across the sky
  3. Adjusts the airplane's position and rotation based on user input
  4. Animates the propeller by continuously rotating it

React Implementation

While the core Three.js functionality remains the same, I've restructured the demo to work within React's component model:

  • Each 3D object (Airplane, Cloud, Sea, Sky) is encapsulated in its own class under /classes
  • Scene setup and animation loop logic is abstracted into /utils/scene.js
  • Colors are maintained as constants in /constants/colors.js
  • The main component in index.jsx handles:
    • Component lifecycle (mounting/unmounting)
    • Event listeners for window resize and mouse/touch input
    • Canvas rendering and animation frame management

This modular structure makes the code more maintainable and reusable, while still preserving the original demo's visual appeal and interactivity.

Viewport and Resize Handling

The demo needs to be responsive and handle window resizing gracefully. This is managed through a combination of React hooks and Three.js viewport calculations:

const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

const viewport = {
  width: dimensions.width,
  height: dimensions.height,
  camera: {
    fov: 60,
    near: 1,
    far: 10000,
    position: { x: 0, y: 100, z: 200 }
  }
};

// Update dimensions when container size changes
useEffect(() => {
  if (!containerRef.current) return;

  const updateDimensions = () => {
    const container = containerRef.current;
    const rect = container.getBoundingClientRect();
    setDimensions({
      width: rect.width,
      height: rect.height
    });
  };

  updateDimensions();
  window.addEventListener('resize', updateDimensions);
  return () => window.removeEventListener('resize', updateDimensions);
}, []);

// Update renderer and camera when viewport changes
useEffect(() => {
  // ...scene setup code...

  const handleResize = () => {
    renderer.setSize(viewport.width, viewport.height);
    camera.aspect = viewport.width / viewport.height;
    camera.updateProjectionMatrix();
  };

  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize);
    if (containerRef.current) {
      containerRef.current.removeChild(renderer.domElement);
    }
    renderer.dispose();
  };
}, [viewport]);

Key aspects of the viewport handling:

  1. Dimension State: Using React's useState to track container dimensions.
  2. Viewport Config: A configuration object that combines dimensions with camera settings.
  3. Resize Observer: Using getBoundingClientRect() to accurately measure the container.
  4. Cleanup: Proper cleanup of event listeners and Three.js resources.

This setup ensures the demo remains responsive and properly scaled across different screen sizes and window resizing events.