Buildings Wave Animation with Three.js

A tutorial on how to create a 3D building wave animation with three.js and TweenMax.

This tutorial is going to demonstrate how to build a wave animation effect for a grid of building models using three.js and TweenMax (GSAP).

Attention: This tutorial assumes you already have a some understanding of how three.js works.
If you are not familiar, I highly recommend checking out the official documentation and examples .

Inspiration

Source: View
by: Baran Kahyaoglu

Core Concept

The idea is to create a grid of random buildings, that reveal based on their distance towards the camera. The motion we are trying to get is like a wave passing through, and the farthest elements will be fading out in the fog.

side-view

We also modify the scale of each building in order to create some visual randomness.

Getting started

First we have to create the markup for our demo. It’s a very simple boilerplate since all the code will be running inside a canvas element:

<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="target" content="all">
    <meta http-equiv="cleartype" content="on">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="mobile-web-app-capable" content="yes">
    <title>Buildings Wave</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/100/three.min.js">></script>
    <script src="https://Threejs.org/examples/js//loaders/OBJLoader.js" ></script>
    <script src="https://Threejs.org/examples/js/controls/OrbitControls.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>
  </head>
  <body>
  </body>
</html>

Basic CSS Styles

html, body {
  margin: 0;
  padding: 0;
  background-color: #fff;
  color: #fff;
  box-sizing: border-box;
  overflow: hidden;
}

canvas {
  width: 100%;
  height: 100%;
}

Initial setup of the 3D world

We create a function called init inside our main class. All subsequent methods will be added inside this method.


init() {
  this.group = new THREE.Object3D();
  this.gridSize = 40;
  this.buildings = [];
  this.fogConfig = {
    color: '#fff',
    near: 1,
    far: 138
  };
}

Creating our 3D scene

createScene() {
  this.scene = new THREE.Scene();

  this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  this.renderer.setSize(window.innerWidth, window.innerHeight);

  this.renderer.shadowMap.enabled = true;
  this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;

  document.body.appendChild(this.renderer.domElement);

  // this is the line that will give us the nice foggy effect on the scene
  this.scene.fog = new THREE.Fog(this.fogConfig.color, this.fogConfig.near, this.fogConfig.far);
}

Camera

Let’s add a camera for to scene:

createCamera() {
  const width = window.innerWidth;
  const height = window.innerHeight;

  this.camera = new THREE.PerspectiveCamera(20, width / height, 1, 1000);

  // set the distance our camera will have from the grid
  // this will give us a nice frontal view with a little perspective
  this.camera.position.set(3, 16, 111);

  this.scene.add(this.camera);
}

Ground

Now we need to add a shape to serve as the scene’s ground

addFloor() {
  const width = 200;
  const height = 200;
  const planeGeometry = new THREE.PlaneGeometry(width, height);

  // all materials can be changed according to your taste and needs
  const planeMaterial = new THREE.MeshStandardMaterial({
    color: '#fff',
    metalness: 0,
    emissive: '#000000',
    roughness: 0,
  });

  const plane = new THREE.Mesh(planeGeometry, planeMaterial);

  planeGeometry.rotateX(- Math.PI / 2);

  plane.position.y = 0;

  this.scene.add(plane);
}

Load 3D models

Before we can build the grid, we have to load our models.

models

loadModels(path, onLoadComplete) {
  const loader = new THREE.OBJLoader();

  loader.load(path, onLoadComplete);
}

onLoadModelsComplete(model) {
  // our buildings.obj file contains many models
  // so we have to traverse them to do some initial setup

  this.models = [...model.children].map((model) => {
    // since we don't control how the model was exported
    // we need to scale them down because they are very big

    // scale model down
    const scale = .01;
    model.scale.set(scale, scale, scale);

    // position it under the ground
    model.position.set(0, -14, 0);

    // allow them to emit and receive shadow
    model.receiveShadow = true;
    model.castShadow = true;

    return model;
  });

  // our list of models are now setup
}

Ambient Light

addAmbientLight() {
  const ambientLight = new THREE.AmbientLight('#fff');

  this.scene.add(ambientLight);
}

Grid Setup

Now we are going to place those models in a grid layout.

initialgrid

createGrid() {
  // define general bounding box of the model
  const boxSize = 3;

  // define the min and max values we want to scale
  const max = .009;
  const min = .001;

  const meshParams = {
    color: '#fff',
    metalness: .58,
    emissive: '#000000',
    roughness: .18,
  };

  // create our material outside the loop so it performs better
  const material = new THREE.MeshPhysicalMaterial(meshParams);

  for (let i = 0; i < this.gridSize; i++) {
    for (let j = 0; j < this.gridSize; j++) {

      // for every iteration we pull out a random model from our models list and clone it
      const building = this.getRandomBuiding().clone();

      building.material = material;

      building.scale.y = Math.random() * (max - min + .01);

      building.position.x = (i * boxSize);
      building.position.z = (j * boxSize);

      // add each model inside a group object so we can move them easily
      this.group.add(building);

      // store a reference inside a list so we can reuse it later on
      this.buildings.push(building);
    }
  }

  this.scene.add(this.group);

  // center our group of models in the scene
  this.group.position.set(-this.gridSize - 10, 1, -this.gridSize - 10);
}

Spot Light

We also add a SpotLight to the scene for a nice light effect.

spot-light

addSpotLight() {
  const light = { color: '#fff', x: 100, y: 150, z: 100 };
  const spotLight = new THREE.SpotLight(light.color, 1);

  spotLight.position.set(light.x, light.y, light.z);
  spotLight.castShadow = true;

  this.scene.add(spotLight);
}

Point Lights

Let’s add some point lights.

point-lights

addPointLight(params) {
  // sample params
  // {
  //   color: '#00ff00',
  //   intensity: 4,
  //   position: {
  //     x: 18,
  //     y: 22,
  //     z: -9,
  //   }
  // };

  const pointLight = new THREE.PointLight(params.color, params.intensity);

  pointLight.position.set(params.position.x, params.position.y, params.position.z);

  this.scene.add(pointLight);
}

Sort Models

Before we animate the models into the scene, we want to sort them according to their z distance to the camera.

sortBuildingsByDistance() {
  this.buildings.sort((a, b) => {
    if (a.position.z > b.position.z) {
      return 1;
    }

    if (a.position.z < b.position.z) {
      return -1;
    }

    return 0;
  }).reverse();
}

Animate Models

This is the function where we go through our buildings list and animate them. We define the duration and the delay of the animation based on their position in the list.

showBuildings() {
  this.sortBuildingsByDistance();

  this.buildings.map((building, index) => {
    TweenMax.to(building.position, .3 + (index / 350), { y: 1, ease: Power3.easeOut, delay: index / 350 });
  });
}

Here is how a variation with camera rotation looks like:

Credits

Ion D. Filho

My name is Ion Drimba Filho, I'm a Front-End Developer based in Brazil. I’m passionate about developing highly interactive interfaces with front-end technologies (JS, CSS, HTML, Canvas).

Stay in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!