ArticlePosted on April 2012

How to make clouds with CSS 3D

Reading time: 5 minutes

Topics: CSS, 3d transforms, JavaScript

This is a legacy post, ported from the old system:
Images might be too low quality, code might be outdated, and some links might not work.

A tutorial on how to use CSS 3D Transforms to create sprite-based 3D-like clouds like these ones.

Introduction

This tutorial will try to guide you through the steps to create a 3D-like, billboard-based clouds. There are a few advanced topics, mainly how 3D transformations via CSS properties work. If you want to find more information, this is a nice place to begin.

The tutorial is divided into sections, each with a different step to understand and follow the process, with HTML, CSS and Javascript blocks. Each step is based on the previous one, and has a link to test the code. The code in the tutorial is a simplified version of the demos, but the main differences are documented on every section.

  1. Creating a world and a camera
  2. Adding objects to our world
  3. Adding layers to our objects
  4. Making the 3D effect work
  5. Final words

1. Creating a world and a camera

First, we need two div elements: viewport and world. All the rest of the elements will be dynamically created.

Viewport covers the whole screen and acts as the camera plane. Since in CSS 3D Transforms there is no camera per se, think of it as a static sheet of glass through which you see a world that changes orientation relative to you. We'll position all our world objects (or scene) inside it, and that's what will be transformed around.

World is a div that we are going to use to anchor all our 3D elements. Transforming (rotating, translating or scaling) world will transform all our elements. For brevity and from here on, I'm using non-prefixed CSS properties. Use the vendor prefix (-webkit, -moz, -o, -ms, etc.) where appropriate.

This is all the markup we'll need:

Viewport and world container

HTML - index.html

<div id="viewport" >
  <div id="world" ></div>
</div>

These next are our two CSS definitions. It's very important to center the div that contains our scene (world in our case) in the viewport, or the scene will be rendered with an offset! Remember that you are still rotating an element that is positioned inside the document, exactly like any other 2D element.

Viewport and world container

CSS - index.html

#viewport {
  bottom: 0;
  left: 0;
  overflow: hidden;
  perspective: 400;
  position: absolute;
  right: 0;
  top: 0;
}

#world {
  height: 512px;
  left: 50%;
  margin-left: -256px;
  margin-top: -256px;
  position: absolute;
  top: 50%;
  transform-style: preserve-3d;
  width: 512px;
}

Now a bit of code. We initialise our objects, hook to the mousemove event and define updateView():

Linking mouse events and update

JavaScript - index.html

/*
  Defining our variables
  world and viewport are DOM elements,
  worldXAngle and worldYAngle are floats that hold the world rotations,
  d is an int that defines the distance of the world from the camera
*/
var world = document.getElementById( 'world' );
viewport = document.getElementById( 'viewport' );
worldXAngle = 0;
worldYAngle = 0;
d = 0;

/*
  Event listener to transform mouse position into angles
  from -180 to 180 degress, both vertically and horizontally
*/
window.addEventListener( 'mousemove', function( e ) {
  worldYAngle = -( .5 - ( e.clientX / window.innerWidth ) ) * 180;
  worldXAngle = ( .5 - ( e.clientY / window.innerHeight ) ) * 180;
  updateView();
} );

/*
  Changes the transform property of world to be
  translated in the Z axis by d pixels,
  rotated in the X axis by worldXAngle degrees and
  rotated in the Y axis by worldYAngle degrees.
*/
function updateView() {
  world.style.transform = 'translateZ( ' + d + 'px ) \
  rotateX( ' + worldXAngle + 'deg) \
  rotateY( ' + worldYAngle + 'deg)';
}

World has red colour, viewport has a CSS background to simulate the sky, and there's mousewheel event listener to modify the distance of the camera. Move the mouse and notice how the red div changes orientation.

2. Adding objects to our world

Now we start adding real 3D content. We add some new div which are positioned in the space, relatively to world. It's esentially adding several absolute-positioned div as children of world, but using translate in 3 dimensions instead of left and top. They are centered in the middle of world by default. The width and height don't really matter, since these new elements are containers for the actual cloud layers. For commodity, it's better to center them (by setting margin-left and margin-top to negative half of width and height).

Cloud base styling

CSS - index.html

.cloudBase {
  height: 20px;
  left: 256px;
  margin-left: -10px;
  margin-top: -10px;
  position: absolute;
  top: 256px;
  width: 20px;
}

We add generate() and createCloud() functions to populate world. Note that random_{var} are not real variables but placeholder names for the real code, which should return a random number between the specified range.

Create and distribute clouds

JavaScript - index.html

/*
  objects is an array of cloud bases
  layers is an array of cloud layers
*/
var objects = [],
layers = [];
/*
  Clears the DOM of previous clouds bases
  and generates a new set of cloud bases
*/
function generate() {
  objects = [];
  layers = [];
  if ( world.hasChildNodes() ) {
    while ( world.childNodes.length >= 1 ) {
      world.removeChild( world.firstChild );
    }
  }

  for( var j = 0; j < 5; j++ ) {
    objects.push( createCloud() );
  }

}

/*
  Creates a single cloud base: a div in world
  that is translated randomly into world space.
  Each axis goes from -256 to 256 pixels.
*/
function createCloud() {
  var div = document.createElement( 'div'  );
  div.className = 'cloudBase';
  var t = 'translateX( ' + random_x + 'px ) \
    translateY( ' + random_y + 'px ) \
    translateZ( ' + random_z + 'px )';
  div.style.transform = t;
  world.appendChild( div );
  return div;
}

The cloud bases are slightly pink squares. There's a p variable to make it easier to change viewport.style.perspective. Try changing this value and notice how it behaves like the FOV of our camera. The higher this value goes, the more orthographic the view goes. Again, note that random_{var} are not variables.

3. Adding layers to our objects

Now things start getting interesting. We add several absolute-positioned .cloudLayer div elements to each .cloudBase. These will hold our cloud textures.

Cloud layers styling

CSS - index.html

.cloudLayer {
  height: 256px;
  left: 50%;
  margin-left: -128px;
  margin-top: -128px;
  position: absolute;
  top: 50%;
  width: 256px;
}

The old createCloud() changes a bit to add a random number of cloudLayers.

Improved createCloud() to add layers

JavaScript - index.html

/*
  Creates a single cloud base and adds several cloud layers.
  Each cloud layer has random position ( x, y, z ), rotation (a)
  and rotation speed (s). layers[] keeps track of those divs.
*/
function createCloud() {
  var div = document.createElement( 'div'  );
  div.className = 'cloudBase';
  var t = 'translateX( ' + random_x + 'px ) \
    translateY( ' + random_y + 'px ) \
    translateZ( ' + random_z + 'px )';
  div.style.transform = t;
  world.appendChild( div );
  for( var j = 0; j < 5 + Math.round( Math.random() * 10 ); j++ ) {
    var cloud = document.createElement( 'div' );
    cloud.className = 'cloudLayer';
    cloud.data = {
      x: random_x,
      y: random_y,
      z: random_z,
      a: random_a,
      s: random_s
    };
    var t = 'translateX( ' + random_x + 'px ) \
      translateY( ' + random_y + 'px ) \
      translateZ( ' + random_z + 'px ) \
      rotateZ( ' + random_a + 'deg ) \
      scale( ' + random_s + ' )';
    cloud.style.transform = t;

    div.appendChild( cloud );
    layers.push( cloud );
  }

  return div;
}

The cloud layers are blue and have a white border, to make them very visible. Move the mouse to notice how each layer is positioned and rotated.

4. Making the 3D effect work

This is where the magic happens. We have layers[] containing a reference for every single layer sprite in our world, and we have worldXangle and worldYAngle, which are the rotation part of the transformation applied to our world.

If we apply the opposite rotation to each and every layer, we are effectively re-aligning them to the viewport: we have a billboard. Since we've rotated the world first in X and then in Y, we need to rotate each layer the opposite way: first Y and then X. The order of transformations is very important. If you don't apply them in the correct order your elements will be incorrectly oriented!

Aligning cloud layers with camera

JavaScript - index.html

/*
  Iterate layers[], update the rotation and apply the
  inverse transformation currently applied to the world.
  Notice the order in which rotations are applied.
*/
function update (){
  for( var j = 0; j < layers.length; j++ ) {
    var layer = layers[ j ];
    layer.data.a += layer.data.speed;
    var t = 'translateX( ' + layer.data.x + 'px ) \
      translateY( ' + layer.data.y + 'px ) \
      translateZ( ' + layer.data.z + 'px ) \
      rotateY( ' + ( - worldYAngle ) + 'deg ) \
      rotateX( ' + ( - worldXAngle ) + 'deg ) \
      scale( ' + layer.data.s + ')';
    layer.style.transform = t;
  }

  requestAnimationFrame( update );

}

Move the mouse around and you'll see that the cloud layers (blue) are now billboards (they always face the camera/viewport), while world and each cloud base are still 3D-projected.

5. Final words

For the final effect, just remove the debug colours and change the cloudLayer div for an img with a cloud texture. The textures should be PNG with alpha channel to get the effect right.

Of course, you can use any texture or set of textures you want: smoke puffs, plasma clouds, green leaves, flying toasters... Just change the background-image that a specific kind of cloud layer uses. Mixing different textures in different proportions gives interesting results.

Adding elements in random order is fine, but you can also create ordered structures, like trees, duck-shaped clouds or complex explosions. Try following a 3D curve and create solid trails of clouds. Create a multiplayer game to guess the shape of a 3D cloud. The possibilities are endless!

I hope it's been an interesting tutorial and not too hard to follow. Feel free to use the code for your project, or to drop me a line!

Comments