three.jsjavascriptES6socket.io

Three.js and Socket.io - Making a planet

Published at 2015-10-27

I wanted to to do something fun. In my last experiment, I played fiddled a little bit around with socket.io to create an interactive chatroom: ChickenChat. The ChickenChat was an idea that I liked and taught it was pretty funny. I wanted to make it a bit more interactive! So I decided to make a planet! An animal planet. A planet of cows...

Make sure to check out the code on github and codepen!

After my earlier experiment in Three.js and Socket.io called ChickenChat, I started dwelling on how I could make a more durable application. I believe that I could've made it in a more effective manner in terms of websockets and blender/animation.

Optimizing models

In the ChickenChat, the blender model file sizes for the chicken was way too big. That was because it relied on a morph animation. This basically means that it stores every frame of animation in the model file. The animation was 20 frames. We made the chicken in 3 parts; body, foot 1 and foot 2. In total, these files were 500 kilobytes. So the application took too long to load.

In the animal planet, I wanted to use a skeletal based animation. There are two reasons for this:

This drastically reduced the file size! Our new cow model is now only 22 kilobytes. And we now have more control of the animation since it is now dynamically triggered. We rotate these bones around per frame and this is only triggered when the cow is moving. If our want to download at the cow model, the blender file is available on GitHub.

The planet is a sphere in blender. I merely dragged out some of the vertices of this sphere. Then I changed some of the face materials to look like grass, mountains and snow. And that's it! No fancy animation. Just the model of this low poly planet. We export this and load it in our solution! A 3D planet

Movement on server

The biggest issue with the ChickenChat was that the server consistently crashed after a lot of activity. The chickens respawned and left behind "ghost chickens". OoOoOo... This was a major annoyance among the users and needed a fix. Surprisingly, I did not do a lot of changes on the socket.io server. The ChickenChat application used the keyboard events to move around. This caused a bigger requirement for movement being accurate. So I sent in every movement. That's almost 1 per 60th millisecond. Times that with all the users in the application and a fairly weak server, and the server will cry and go to sleep.

In the planet, we removed the keyboard movement and replaced it with a click based movement. Now the server only receives data about movement when the other users clicks. A target. The client handles this response and moves the cow towards the target. Neat!

Another new thing in the planet is that we detect where the user wants to move with a raycast. The raycast works is a projected beam that detects mesh that it collides with. It collides with our planet model. So our click event essentially uses this to find the target position.

onMouseUp(event){
	let self = this;
 	let x = event.clientX;
        let y = event.clientY;
	
	var mouse = {	
		x: ( x / window.innerWidth ) * 2 - 1,
		y: - ( y / window.innerHeight ) * 2 + 1
	}
	this.raycaster.setFromCamera( mouse, this.camera );

	var intersects = this.raycaster.intersectObject( this.world.mesh );
	
	if(intersects.length){
		var point = intersects[ 0 ].point;
		var newpoint = new THREE.Vector3(point.x / self.animal.scale, point.y / self.animal.scale, point.z / self.animal.scale);
		self.animal.moveTowardsTarget(newpoint);
		
		self.socket.emit("move animal", { 
			x: newpoint.x,
			y: newpoint.y,
			z: newpoint.z
		});
	}
	
}

After the click event we need to trigger our movement logic. This function moves against the target position, but follows the mesh on the path. This is also a raycast from the cow position and it goes straight down! This keeps our cow grounded and avoids the whole flying/floating issue.

updateMovement(mesh){
	this.body.lookAt( this.target );
	this.body.translateZ(0.5);
	let pos = new THREE.Vector3(this.body.position.x*1.05 * this.scale, this.body.position.y*1.05 * this.scale, this.body.position.z*1.05 * this.scale);
	let center = new THREE.Vector3( 0, 0, 0).sub(pos).normalize();
	let raycaster = new THREE.Raycaster(pos, center);
	let intersects = raycaster.intersectObject( mesh );
	
	if(intersects.length){
		let point = intersects[ 0 ].point;
		let newpoint = new THREE.Vector3(point.x / this.scale, point.y / this.scale, point.z / this.scale);
		this.body.position.copy(newpoint);
		let groundpoint = intersects[ 0 ].face.normal;
		let v1 = this.target.clone().sub(this.body.position).normalize();
  		let v2 = groundpoint.clone().sub(this.body.position).normalize();
  		let v3 = new THREE.Vector3().crossVectors(v1, v2).normalize();
  		this.body.up.copy( v3 );
		this.body.lookAt(groundpoint);	
	}

Result

The result looks a little like this! I'm really excited on how this turned out. I hope that you check this out and perhaps make some new friends along the way! I plan to implement support for multiple animals and maybe water in the future. Let me know if there is something you want to be added to this experiment! Make sure to check out the code on github and codepen! Cows walking on planet See you on another planet!

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.