ES6javascriptthree.js

Making chickens and eggs in three.js, blender and babel

Published at 2015-09-20

I had this idea. How do I make an interesting example of exponential growth in 3D? Chicken and eggs? I have been playing with three.js for a little while. It's an amazing tool for 3D visualization and I have to give credits to Ricardo Cabello for creating such a great library. So I created this Javascript experiment that will possibly destroy your browser after a while!

You can check out this example on codepen.

The pretty chicken

I'm started picking up blender for this small project. I used a finished model of a chicken from the mobile game Crossy Road. All props to ERRAWR for this model. All I did was animating the chicken. Animated blocky chicken After making the chicken move, I exported three components with the three.js json exporter for blender. Body, foot 1 and foot 2. This is because they have separate animation. We are later going to use this model to build our chicken in three.js.

Gulp and ES6

In the theme of experimental, I write all the Javascript in this application in ES6. This is transpiled with babel and browserify in my local solution. All building is done in Gulp. Here is the gulpfile.js:

var gulp = require('gulp');
var browserify = require('browserify');
var babelify= require('babelify');
var util = require('gulp-util');
var buffer = require('vinyl-buffer');
var source = require('vinyl-source-stream');
var uglify = require('gulp-uglify');
var sourcemaps = require('gulp-sourcemaps');

gulp.task('build', function() {
  browserify('./src/app.js', { debug: true })
  .add(require.resolve('babel/polyfill'))
  .transform(babelify)
  .bundle()
  .on('error', util.log.bind(util, 'Browserify Error'))
  .pipe(source('compiled.js'))
  .pipe(buffer())
  .pipe(sourcemaps.init({loadMaps: true}))
  .pipe(uglify({ mangle: false }))
  .pipe(sourcemaps.write('./'))
  .pipe(gulp.dest('./dist'));
});

gulp.task('watch', function() {
  gulp.watch('./src/*.js', ['build']);
});

gulp.task('default', ['build', 'watch']);

It's a pretty standard setup of babel running with browserify as a module loader. The source maps allows us to debug the application in ES6 and seperate modules. Which is pretty cool! The gulp starts in the app.js file and finds all references on the fly.

It all starts in the app.js file

Our app.js file is simply our initialization point. It triggers an object/class called Playground with new Playground(). Playground is where we tie together all the magic and is where the three.js logic lives. In the constructor we set up the scene:

//PLAYGROUND.JS
let self = this;

this.chickens = [];

this.clock = new THREE.Clock()
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 10000);

this.ambientLight = new THREE.AmbientLight( "#FFFFF3" );
this.scene.add( this.ambientLight );
this.directionalLight = new THREE.DirectionalLight( 0xffffff, 1 );
this.directionalLight.position.set( 1, 0.75, 0.5 ).normalize();
this.scene.add( this.directionalLight );
this.renderer = new THREE.WebGLRenderer( { antialias: true } );
this.camera.position.set( 900, 900, 900 );
this.camera.target = new THREE.Vector3( 0, 0, 0 );
this.camera.lookAt( this.camera.target );
this.controls = new THREE.OrbitControls( this.camera, this.renderer.domElement );
this.controls.damping = 0.2;
this.controls.maxPolarAngle = Math.PI/2;
this.controls.minDistance = 500;

this.scene.add(this.camera);
this.renderer.setClearColor( "#CCCCCC" );
this.renderer.sortObjects = false;
this.renderer.autoClear = false;
this.renderer.gammaInput = true;
this.renderer.gammaOutput = true;
this.renderer.setPixelRatio( window.devicePixelRatio );
this.renderer.setSize( window.innerWidth, window.innerHeight );
let container = document.createElement( 'div' );
document.body.appendChild(container);
container.appendChild( this.renderer.domElement );

this.draw();
this.reference = new LoadModels();
this.reference.load().then(function(){
	new Chicken(0, 0, 15, self.reference, self.scene, self.chickens);
})

This is a standard setup for a three.js scene. Some lights, camera targets and controls. In the end of the setup we introduce two new classes, LoadModels and Chicken.

//LOADMODELS.JS
export class LoadModels{
	constructor(){
		this.foot1 = null;
		this.foot2 = null;
		this.body = null;
		
	}
	load(){
		let self = this;
		return new Promise(function (fulfill, reject){
			let loader = new THREE.JSONLoader();
			loader.load('body.json', function ( geometry, materials ) {
				materials.forEach(function(e){
					e.morphTargets = true;
				});
				let material = new THREE.MeshFaceMaterial( materials );
				self.body = new THREE.MorphAnimMesh( geometry, material );
				self.body.duration = 1200; 
				
				loader.load('foot1.json', function ( geometry, materials ) {
					materials.forEach(function(e){
						e.morphTargets = true;
					});
					let material = new THREE.MeshFaceMaterial( materials );
					self.foot1 = new THREE.MorphAnimMesh( geometry, material );
					self.foot1.duration = 1200;
					
					loader.load('foot2.json', function ( geometry, materials ) {
						materials.forEach(function(e){
							e.morphTargets = true;
						});
						let material = new THREE.MeshFaceMaterial( materials );
						self.foot2 = new THREE.MorphAnimMesh( geometry, material );
						self.foot2.duration = 1200;
						fulfill();
					});
				});
			});
		});
	}
}

This class loads the json files and makes the data into animation and meshes. This is wrapped inside a promise call that fulfills after the models are loaded. We reuse this for each chicken that is spawned. We are using the MorphAnimMesh class with our keyframe animation in Blender.

//CHICKEN.JS
export class Chicken{
	constructor(x, z, scale, reference, scene, chickens){
		let self = this;
		this.scale = scale;
		this.position = new THREE.Vector3(x, 0, z);
		this.group = new THREE.Group();
		this.scene = scene;
		this.speed = 3;
		this.rotation = 0;
		this.chickens = chickens;
		this.body = reference.body.clone()
		this.foot1 = reference.foot1.clone()
		this.foot2 = reference.foot2.clone()
		setInterval(() => {
			let randomness = Math.random();
			self.group.children.forEach(function(model){
				model.rotation.y += Math.PI / 2 * randomness;
			});
		}, Math.floor(Math.random() * 7000) + 3000);
		
		setInterval(() => {
			let o = self.body.position;
			let x = self.position.x !== 0 ? self.position.x / self.scale : self.position.x;
			let z = self.position.z !== 0 ? self.position.z / self.scale : self.position.z;
			let egg = new Egg(o.x + x, o.z + z, self.scale, self.scene);
			egg.incubate().then((pos)=>{
				new Chicken(pos.x, pos.z, self.scale, reference, self.scene, self.chickens)
			});
		}, Math.floor(Math.random() * 7000) + 3000);
		
		this.loadModel();
		this.chickens.push(this);
	}
	
	loadModel(){
		this.group.add(this.body);
		this.group.add(this.foot1);
		this.group.add(this.foot2);
		this.group.scale.set( this.scale, this.scale, this.scale );
		this.group.position.set(this.position.x, this.position.y, this.position.z);
		this.scene.add(this.group);
	}
}

A lot of things goes on in this file. But shortly explained, this spawns, decides walking patterns and "egg laying" times for a single chicken. All parts of the chicken is added to a group. This class introduces the "egg" class:

//EGG.JS
export class Egg{
	constructor(x, z, scale, scene){
		this.hatchtime = (Math.floor(Math.random() * 12) + 4) * 1000;
		var geometry = new THREE.BoxGeometry( 15, 24, 15 );
		var material = new THREE.MeshBasicMaterial( {color: 0xffffff} );
		this.mesh = new THREE.Mesh(geometry, material)
		this.mesh.position.set(x * scale, -15, z * scale);
		this.scene = scene;
		this.scene.add(this.mesh);
	}
	
	incubate(){
		let self = this;
		return new Promise((fulfill, reject)=>{
			setTimeout(()=>{
				fulfill(self.mesh.position);
				self.scene.remove(self.mesh);
			}, self.hatchtime);
		});
	}
}

This spawns an egg as a simple box and adds it to the scene. It contains an incubate method that returns a Promise. After a random amount of time, the egg will hatch and the egg will be removed from the scene.

Move that chicken!

We animate and move the chickens through an animation loop. Per frame we loop through the chicken array to find every chicken.

//PLAYGROUND.JS
draw(){
	let self = this;
	window.requestAnimationFrame(function(){self.draw();});
	
	let delta = this.clock.getDelta();
	
	this.chickens.forEach(function(model){
		model.group.children.forEach(function(mesh){
			mesh.updateAnimation( 1000 * delta );
			mesh.translateX( model.speed * delta )
		});
	});
	this.renderer.clear();
	this.renderer.setViewport( 0, 0, window.innerWidth, window.innerHeight );
	this.renderer.render(this.scene, this.camera);
}

Finished

And that's it! That's the key elements of this chicken army. The results should look like this: Chickens laying eggs And it keeps growing! Until your browser breaks. You can check out this full example on codepen.

Avatar of Author

Karl SolgÄrd

Norwegian software developer. Eager to learn and to share knowledge. Sharing is caring! Follow on social: Twitter and LinkedIn.