Создание игры «Aviator»: изучаем основы 3D-анимации c Three.js. Часть вторая. Наводим красоту

Рассказывает Карим Маалул 


Почти готово!

Как вы уже убедились в прошлой части, Three.js сильно облегчает работу с WebGL. Вам не нужны обширные знания для настройки сцены и отрисовки сложных объектов. Пока что вы изучили несколько основных принципов, но пора двигаться дальше. 

В этой части мы улучшим визуальное качество сцены, сделаем движение самолёта гораздо более плавным и добавим эффект волн.

Самолёт на прокачку

Наш самолёт пока что очень прост. Пока что мы умеем только создавать объекты и комбинировать их, но мы пока не умеем подгонять их под свои нужды.

Например, куб можно изменить, сдвинув его вершины. В нашем случае нужно сделать его более похожим на фюзеляж. Вот что должно получиться:

// Cockpit

var geomCockpit = new THREE.BoxGeometry(80,50,50,1,1,1);
var matCockpit = new THREE.MeshPhongMaterial({color:Colors.red, shading:THREE.FlatShading});

// we can access a specific vertex of a shape through 
// the vertices array, and then move its x, y and z property:
geomCockpit.vertices[4].y-=10;
geomCockpit.vertices[4].z+=20;
geomCockpit.vertices[5].y-=10;
geomCockpit.vertices[5].z-=20;
geomCockpit.vertices[6].y+=30;
geomCockpit.vertices[6].z+=20;
geomCockpit.vertices[7].y+=30;
geomCockpit.vertices[7].z-=20;

var cockpit = new THREE.Mesh(geomCockpit, matCockpit);
cockpit.castShadow = true;
cockpit.receiveShadow = true;
this.mesh.add(cockpit);

Вот пример того, как изменять фигуры. Если вы изучите исходный код самолёта, то найдёте там ещё несколько других объектов и более приятный пропеллер. Ничего сложного.

Но кто управляет самолётом?

Посадить пилота в самолёт — как два куба добавить.

Но нам нужен не просто пилот, а крутой пилот с причёской, развевающейся на ветру. Звучит сложно, но поскольку мы работаем с низкополигональными моделями, реализация не вызовет проблем.

Давайте взглянем на код:

var Pilot = function(){
	this.mesh = new THREE.Object3D();
	this.mesh.name = "pilot";
	
	// angleHairs is a property used to animate the hair later 
	this.angleHairs=0;

	// Body of the pilot
	var bodyGeom = new THREE.BoxGeometry(15,15,15);
	var bodyMat = new THREE.MeshPhongMaterial({color:Colors.brown, shading:THREE.FlatShading});
	var body = new THREE.Mesh(bodyGeom, bodyMat);
	body.position.set(2,-12,0);
	this.mesh.add(body);

	// Face of the pilot
	var faceGeom = new THREE.BoxGeometry(10,10,10);
	var faceMat = new THREE.MeshLambertMaterial({color:Colors.pink});
	var face = new THREE.Mesh(faceGeom, faceMat);
	this.mesh.add(face);

	// Hair element
	var hairGeom = new THREE.BoxGeometry(4,4,4);
	var hairMat = new THREE.MeshLambertMaterial({color:Colors.brown});
	var hair = new THREE.Mesh(hairGeom, hairMat);
	// Align the shape of the hair to its bottom boundary, that will make it easier to scale.
	hair.geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0,2,0));
	
	// create a container for the hair
	var hairs = new THREE.Object3D();

	// create a container for the hairs at the top 
	// of the head (the ones that will be animated)
	this.hairsTop = new THREE.Object3D();

	// create the hairs at the top of the head 
	// and position them on a 3 x 4 grid
	for (var i=0; i<12; i++){
		var h = hair.clone();
		var col = i%3;
		var row = Math.floor(i/3);
		var startPosZ = -4;
		var startPosX = -4;
		h.position.set(startPosX + row*4, 0, startPosZ + col*4);
		this.hairsTop.add(h);
	}
	hairs.add(this.hairsTop);

	// create the hairs at the side of the face
	var hairSideGeom = new THREE.BoxGeometry(12,4,2);
	hairSideGeom.applyMatrix(new THREE.Matrix4().makeTranslation(-6,0,0));
	var hairSideR = new THREE.Mesh(hairSideGeom, hairMat);
	var hairSideL = hairSideR.clone();
	hairSideR.position.set(8,-2,6);
	hairSideL.position.set(8,-2,-6);
	hairs.add(hairSideR);
	hairs.add(hairSideL);

	// create the hairs at the back of the head
	var hairBackGeom = new THREE.BoxGeometry(2,8,10);
	var hairBack = new THREE.Mesh(hairBackGeom, hairMat);
	hairBack.position.set(-1,-4,0)
	hairs.add(hairBack);
	hairs.position.set(-5,5,0);

	this.mesh.add(hairs);

	var glassGeom = new THREE.BoxGeometry(5,5,5);
	var glassMat = new THREE.MeshLambertMaterial({color:Colors.brown});
	var glassR = new THREE.Mesh(glassGeom,glassMat);
	glassR.position.set(6,0,3);
	var glassL = glassR.clone();
	glassL.position.z = -glassR.position.z

	var glassAGeom = new THREE.BoxGeometry(11,1,11);
	var glassA = new THREE.Mesh(glassAGeom, glassMat);
	this.mesh.add(glassR);
	this.mesh.add(glassL);
	this.mesh.add(glassA);

	var earGeom = new THREE.BoxGeometry(2,3,2);
	var earL = new THREE.Mesh(earGeom,faceMat);
	earL.position.set(0,0,-6);
	var earR = earL.clone();
	earR.position.set(0,0,6);
	this.mesh.add(earL);
	this.mesh.add(earR);
}

// move the hair
Pilot.prototype.updateHairs = function(){
	
	// get the hair
	var hairs = this.hairsTop.children;

	// update them according to the angle angleHairs
	var l = hairs.length;
	for (var i=0; i<l; i++){
		var h = hairs[i];
		// each hair element will scale on cyclical basis between 75% and 100% of its original size
		h.scale.y = .75 + Math.cos(this.angleHairs+i/3)*.25;
	}
	// increment the angle for the next frame
	this.angleHairs += 0.16;
}

Чтобы волосы двигались, добавьте эту строку в цикл:

airplane.pilot.updateHairs();

Ловим волны

Вы наверняка заметили что море больше похоже не на море, а на поверхность, по которой проехались катком.

Морю нужны волны. Это можно реализовав, используя две ранее использованные техники:

  • перемещение вершин;
  • цикличное движение каждой вершиныt.

Для создания волн мы будем вращать каждую вершину цилиндра вокруг её начальной позиции, задавая её случайные угловую скорость и радиус вращения. Да, придётся вспомнить тригонометрию 🙁

Изменим море:

Sea = function(){
	var geom = new THREE.CylinderGeometry(600,600,800,40,10);
	geom.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI/2));

	// important: by merging vertices we ensure the continuity of the waves
	geom.mergeVertices();

	// get the vertices
	var l = geom.vertices.length;

	// create an array to store new data associated to each vertex
	this.waves = [];

	for (var i=0; i<l; i++){
		// get each vertex
		var v = geom.vertices[i];

		// store some data associated to it
		this.waves.push({y:v.y,
				 x:v.x,
				 z:v.z,
				 // a random angle
				 ang:Math.random()*Math.PI*2,
				 // a random distance
				 amp:5 + Math.random()*15,
				 // a random speed between 0.016 and 0.048 radians / frame
				 speed:0.016 + Math.random()*0.032
				});
	};
	var mat = new THREE.MeshPhongMaterial({
		color:Colors.blue,
		transparent:true,
		opacity:.8,
		shading:THREE.FlatShading,
	});

	this.mesh = new THREE.Mesh(geom, mat);
	this.mesh.receiveShadow = true;

}

// now we create the function that will be called in each frame 
// to update the position of the vertices to simulate the waves

Sea.prototype.moveWaves = function (){
	
	// get the vertices
	var verts = this.mesh.geometry.vertices;
	var l = verts.length;
	
	for (var i=0; i<l; i++){
		var v = verts[i];
		
		// get the data associated to it
		var vprops = this.waves[i];
		
		// update the position of the vertex
		v.x = vprops.x + Math.cos(vprops.ang)*vprops.amp;
		v.y = vprops.y + Math.sin(vprops.ang)*vprops.amp;

		// increment the angle for the next frame
		vprops.ang += vprops.speed;

	}

	// Tell the renderer that the geometry of the sea has changed.
	// In fact, in order to maintain the best level of performance, 
	// three.js caches the geometries and ignores any changes
	// unless we add this line
	this.mesh.geometry.verticesNeedUpdate=true;

	sea.mesh.rotation.z += .005;
}

Добавим эту строчку в цикл, как мы сделали с волосами:

sea.moveWaves();

Наслаждайтесь волнами!

Улучшаем освещение сцены

В первой части руководства мы уже настроили освещение. Настало время сделать его более мягким. Для этого мы используем рассеянное освещение.

В функции createLights добавим следующее:

// an ambient light modifies the global color of a scene and makes the shadows softer
ambientLight = new THREE.AmbientLight(0xdc8874, .5);
scene.add(ambientLight);

Не бойтесь экспериментировать с цветами и интенсивностью рассеянного света.

Мягкий полёт

Наш маленький самолёт уже следует за движениями мыши, но это не похоже на полёт. Нужно, чтобы он мягче изменял своё положение. В этой, последней части руководства мы займёмся именно этим.

Этого можно достичь, добавляя в каждом кадре часть расстояния между самолётом и целью. Общий вид кода таков (не добавляйте его в программу, это лишь шаблон):

currentPosition += (finalPosition - currentPosition)*fraction;

Для большей реалистичности вращение самолёта также должно изменяться в зависимости от направления движения. Если самолёт быстро поднимается, то он должен быстро вращаться против часовой стрелки. Если он медленно опускается, то должен медленно вращаться по часовой стрелке.

Вот реализация всех рассуждений в функции updatePlane:

function updatePlane(){
	var targetY = normalize(mousePos.y,-.75,.75,25, 175);
	var targetX = normalize(mousePos.x,-.75,.75,-100, 100);
	
	// Move the plane at each frame by adding a fraction of the remaining distance
	airplane.mesh.position.y += (targetY-airplane.mesh.position.y)*0.1;

	// Rotate the plane proportionally to the remaining distance
	airplane.mesh.rotation.z = (targetY-airplane.mesh.position.y)*0.0128;
	airplane.mesh.rotation.x = (airplane.mesh.position.y-targetY)*0.0064;

	airplane.propeller.rotation.x += 0.3;
}

Теперь движение самолёта выглядит гораздо элегантней и реалистичней. Изменяя дробные величины, можно регулировать резкость движений.

Вот как выглядит наш проект: демонстрация, часть 2

Отлично!

Что дальше?

Вы изучили базовые, но универсальные приёмы Three.js, которые позволяют вам создавать простые сцены. Вы научились создавать объекты из примитивов, анимировать их и настраивать освещение сцены.

Следующим этапом, который не вошёл в этой руководство по причине использования сложных техник, будет реализация игры, в которой используются такие понятия, как коллизии, сбор предметов и управление уровнями. Изучите исходный код: там вы найдёте уже изучите приёмы и новые, более сложные, в которых вам предстоит разобраться.

Сыграть в игру вы можете, нажав на картинку ниже. Она оптимизирована для настольных компьютеров.

Перевод статьи «The Making of "The Aviator": Animating a Basic 3D Scene with Three.js»