ANIMATED WHITE
CLOUD PARTICLES

Build a fluffy, slowly swirling white cloud in Three.js from 70 see-through smoke puffs that always face the camera, over a painted blue sky. No 3D modeling, no extra libraries, about 100 lines.

✓ FREE Level 1 · Beginner Billboard particlesTransparent texturesFog

By Shane Brumback · Updated November 2, 2026 · about 12 minute read

LIVE DEMO

The cloud floating on the right of this page is the live Three.js scene. Drag the empty background to orbit around it, or launch the full-screen demo to drag to orbit and scroll to zoom into the cloud. Launch asks for a free Google sign-in first. Press Esc or Exit to return.

Free Google sign-in · Orbit: drag · Zoom: scroll · Esc to exit

Introduction

Real clouds are not solid objects. They are millions of tiny droplets, and what you see is light scattering through them. Games fake that with a cheap, very effective trick: stack a few dozen flat, see-through pictures of smoke on top of each other, keep every picture turned toward the camera, and let them drift. Your eye reads the overlapping soft edges as a single fluffy volume.

In this free tutorial you will build exactly that: a white cumulus cloud made of 70 smoke-textured puffs, a sky gradient painted with a tiny canvas, sunlight, blue haze, and slow swirling motion. The same technique powers smoke, fog banks, steam, dust and explosions in browser games, so it is worth learning once properly.

How It Works (Key Ideas)

  • Particles — many small, simple pieces that together look like one complex effect. Here each particle is a square plane showing a smoke texture.
  • Billboarding — copying the camera's rotation onto each puff every frame so you always see its face, never its paper-thin edge.
  • Transparency and depthWrite — the PNG's soft alpha edges blend puffs together. Turning off depthWrite stops one puff's invisible corners from hiding the puffs behind it.
  • Shape by spread — random positions in a wide, flat box (560 by 150 by 320) give the classic cumulus silhouette. Change the box to change the cloud.
  • Fog — FogExp2 tints far puffs toward pale blue, adding depth for free.

Performance Notes

All 70 puffs share one geometry and one material, so the GPU work is tiny. What costs time with clouds is overdraw: many transparent layers covering the same pixels. If a phone struggles, lower the puff count to 40 or make the planes smaller before touching anything else. For thousands of particles, switch to THREE.Points or an InstancedMesh.

Step-by-Step Code Breakdown

Every excerpt below is copied exactly from the full code further down, which is the same code running the live scene on this page.

1. Scene, camera and renderer

In plain words: we make an empty 3D world, a camera far enough back to see a whole cloud, and a renderer that draws it into the page. Pale blue fog makes the far side of the cloud fade softly, the way real air does.

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0xbcd8f0, 0.0006);   // soft blue haze

const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 3000);
camera.position.set(0, 40, 900);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);

2. Paint a sky gradient

In plain words: instead of loading a sky photo, we paint a tiny 2 x 256 pixel picture that goes from deep blue at the top to pale blue at the bottom, and stretch it across the background. It costs almost nothing to draw.

const sky = document.createElement('canvas');
sky.width = 2; sky.height = 256;
const g = sky.getContext('2d');
const grad = g.createLinearGradient(0, 0, 0, 256);
grad.addColorStop(0, '#2c64ad');   // deep blue overhead
grad.addColorStop(1, '#bfdcf3');   // pale near the horizon
g.fillStyle = grad;
g.fillRect(0, 0, 2, 256);
const skyTex = new THREE.CanvasTexture(sky);
skyTex.colorSpace = THREE.SRGBColorSpace;
scene.background = skyTex;

3. Light the cloud

In plain words: clouds are lit by the whole sky plus the sun. A bright ambient light stands in for the sky, and a warm directional light from the upper left adds the sunny side.

scene.add(new THREE.AmbientLight(0xffffff, 1.3));
const sun = new THREE.DirectionalLight(0xfff0d8, 2.2);
sun.position.set(-1, 1, 1);
scene.add(sun);

4. Build the cloud from smoke puffs

In plain words: a cloud here is just 70 flat squares, each showing the same soft smoke picture. Because the picture is see-through at the edges, the overlapping squares blend into one fluffy shape. We spread them wide and flat like a real cumulus cloud, and give each one its own slow spin speed. depthWrite: false stops the see-through edges from cutting holes in each other.

const smoke = new THREE.TextureLoader().load('/images/smoke3.png');
smoke.colorSpace = THREE.SRGBColorSpace;
const puffGeo = new THREE.PlaneGeometry(320, 320);
const puffMat = new THREE.MeshLambertMaterial({
  map: smoke, transparent: true, opacity: 0.6, depthWrite: false
});
const cloud = new THREE.Group();
const puffs = [];
for (let i = 0; i < 70; i++) {
  const p = new THREE.Mesh(puffGeo, puffMat);
  // wide, flat spread: x is long, y is short, z is medium
  p.position.set((Math.random() - 0.5) * 560, (Math.random() - 0.5) * 150, (Math.random() - 0.5) * 320);
  p.userData = {
    angle: Math.random() * Math.PI * 2,         // starting twist
    spin: (Math.random() - 0.5) * 0.16          // radians per second (slow)
  };
  cloud.add(p);
  puffs.push(p);
}
scene.add(cloud);

5. Animate: billboard, swirl and float

In plain words: every frame each puff turns to face the camera (that trick is called billboarding, so you never see a flat square edge-on), then twists a tiny bit around its middle. The whole cloud floats up and down on a slow sine wave. A Clock measures real seconds so the speed is the same on every computer.

const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;
  for (const p of puffs) {
    p.userData.angle += p.userData.spin * dt;
    p.quaternion.copy(camera.quaternion);    // face the camera (billboard)
    p.rotateZ(p.userData.angle);             // slow swirl
  }
  cloud.position.y = Math.sin(t * 0.3) * 14; // gentle float
  controls.update();
  renderer.render(scene, camera);
}
animate();

The Full Code

Paste this into a file named index.html. Open it through a local web server (for example npx serve) and put smoke3.png in an images folder, because browsers block textures loaded from file://. Launch Full Code opens this exact code as a standalone page in a new window.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Three.js Animated White Cloud</title>
  <style>
    body { margin: 0; overflow: hidden; background: #000; font-family: system-ui, sans-serif; }
  </style>
  <script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
      "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
    }
  }
  </script>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// --- 1. Scene, camera and renderer ---
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0xbcd8f0, 0.0006);   // soft blue haze

const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 3000);
camera.position.set(0, 40, 900);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);

// Orbit controls: drag (mouse or touch) to orbit. Zoom is off on the page so
// vertical swipes still scroll on phones; the full-screen demo turns zoom on.
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.enableZoom = false;
controls.enablePan = false;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.25;
renderer.domElement.style.touchAction = 'pan-y';

// --- 2. Paint a sky gradient ---
const sky = document.createElement('canvas');
sky.width = 2; sky.height = 256;
const g = sky.getContext('2d');
const grad = g.createLinearGradient(0, 0, 0, 256);
grad.addColorStop(0, '#2c64ad');   // deep blue overhead
grad.addColorStop(1, '#bfdcf3');   // pale near the horizon
g.fillStyle = grad;
g.fillRect(0, 0, 2, 256);
const skyTex = new THREE.CanvasTexture(sky);
skyTex.colorSpace = THREE.SRGBColorSpace;
scene.background = skyTex;

// --- 3. Light the cloud ---
scene.add(new THREE.AmbientLight(0xffffff, 1.3));
const sun = new THREE.DirectionalLight(0xfff0d8, 2.2);
sun.position.set(-1, 1, 1);
scene.add(sun);

// --- 4. Build the cloud from smoke puffs ---
const smoke = new THREE.TextureLoader().load('/images/smoke3.png');
smoke.colorSpace = THREE.SRGBColorSpace;
const puffGeo = new THREE.PlaneGeometry(320, 320);
const puffMat = new THREE.MeshLambertMaterial({
  map: smoke, transparent: true, opacity: 0.6, depthWrite: false
});
const cloud = new THREE.Group();
const puffs = [];
for (let i = 0; i < 70; i++) {
  const p = new THREE.Mesh(puffGeo, puffMat);
  // wide, flat spread: x is long, y is short, z is medium
  p.position.set((Math.random() - 0.5) * 560, (Math.random() - 0.5) * 150, (Math.random() - 0.5) * 320);
  p.userData = {
    angle: Math.random() * Math.PI * 2,         // starting twist
    spin: (Math.random() - 0.5) * 0.16          // radians per second (slow)
  };
  cloud.add(p);
  puffs.push(p);
}
scene.add(cloud);

// --- 5. Animate: billboard, swirl and float ---
const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;
  for (const p of puffs) {
    p.userData.angle += p.userData.spin * dt;
    p.quaternion.copy(camera.quaternion);    // face the camera (billboard)
    p.rotateZ(p.userData.angle);             // slow swirl
  }
  cloud.position.y = Math.sin(t * 0.3) * 14; // gentle float
  controls.update();
  renderer.render(scene, camera);
}
animate();

addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>

What To Try Next

  • Change the spread in p.position.set(...) to * 200, * 200, * 200 for a round puffball, or * 1200, * 60 for a long, thin stratus layer.
  • Tint the cloud at sunset: set the sun color to 0xff9a5a and the sky gradient stops to orange and purple.
  • Make several clouds by wrapping the loop in a function and placing each group at a different position, then slide them across the sky.
  • Swap the white smoke for the multi-color cloud version.

Frequently Asked Questions

How do you make clouds in Three.js?

The simplest convincing way is a particle cloud: create many PlaneGeometry meshes that share one transparent smoke texture, scatter them inside a wide, flat area, and rotate each one to face the camera every frame. The overlapping soft edges read as one fluffy cloud.

What is billboarding in Three.js?

Billboarding means turning a flat object so it always faces the camera. In this tutorial each puff copies camera.quaternion every frame and then rotates around its own Z axis for a slow swirl. THREE.Sprite does the same thing automatically.

Why does my transparent cloud have dark or square edges?

Usually depthWrite is on, so the transparent corners of nearer puffs block puffs behind them. Set depthWrite: false on the material. Also make sure the texture is a PNG with a real alpha channel and that transparent: true is set.

Why does the smoke texture not load when I open the file?

Browsers block textures loaded from file:// URLs for security. Run a local web server such as npx serve or the VS Code Live Server extension, and keep smoke3.png at /images/smoke3.png relative to that server.

Is this Three.js cloud tutorial free?

Yes. It is a completely free tutorial. The full code is on the page and the Launch Full Code button opens it in a new window. The full-screen live demo only asks for a free Google sign-in.