From our sponsor: Meco is a distraction-free space for reading and discovering newsletters, separate from the inbox.
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).
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.
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.
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.
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.
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.
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
- Models by Backlog Studio. Check out their Instagram.