Август 2015
Том 30, выпуск 8
Visual Studio 2013 - Введение в трехмерную графику для веб-игр
Продукты и технологии:
Three.js, HTML, JavaScript
В статье рассматриваются:
- рендеринг трехмерных изображений на двухмерном экране;
- добавление перспективы и расстояния к трехмерным объектам;
- анимация и затенение трехмерных объектов.
Добавление третьего измерения в игре по-настоящему оживляет ее. Вы можете смотреть с любой точки обзора и видеть каждый угол какого-нибудь объекта или сцены. Но как добиться этого? В этой серии статей я пошагово пройду процесс создания трехмерных (3D) игр и покажу, как библиотеки вроде three.js могут помочь в формировании богатой 3D-среды, становящейся столь популярной в Web. В первой статье я не буду ничего усложнять и сосредоточусь на 3D-версии игры Ping, впервые описанной в моей статье «A Web Game in an Hour».
Иллюзия трехмерности
Рендеринг 3D-графики обманывает нас, держа туз в рукаве. Люди на самом деле не могут видеть три измерения, особенно на мониторе компьютера. Весь смысл 3D-рисования — генерация, или рендеринг, 3D-описания сцены на 2D-изображении. Добавляя третье измерение для построения более реалистичных сцен, вы должны отбрасывать некоторые данные, чтобы получить изображение с конкретной точки обзора. Эта концепция называется проекцией. Это существенный элемент того, что заставляет работать 3D-графику, как показано на примере базовой 3D-сцены на рис. 1.
Рис. 1. Простая 3D-сцена
В этой сцене ось Z направлена вверх и в обратном направлении. Если бы я хотел действительно увидеть ее на экране, я мог бы просто удалить Z-информацию из каждого объекта — это простой и допустимый способ проецирования 3D-сцены, как показано на рис. 2.
Рис. 2. Расплющенная 3D-сцена
Как видите, это не совсем Halo. Для фотореалистичности 3D-сцена требует трех вещей: должной проекции камеры, геометрии и затенения. Я опишу каждую из этих концепций в процессе переделки игры Ping в трехмерную.
Приступаем к работе
Первым делом я настраиваю библиотеку three.js. Это довольно простая конфигурация, так как почти все, что вы делаете с three.js, происходит вJavaScript. Вот HTML-код, который вам понадобится:
<html>
<head>
<title>Ping!</title>
<script src=
"//cdnjs.cloudflare.com/ajax/libs/three.js/r69/three.min.js"></script>
<script src="ping3d.js"></script>
</head>
<body>
</body>
</html>
В JavaScript-файле ping3d.js я настрою three.js на рендеринг простой сцены. Сначала нужно инициализировать three.js и добавить на страницу холст для рисования:
var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
Сцена — это объект, который описывает нашу сцену и все объекты внутри нее. Рендер тоже достаточно очевиден. Получив сцену, рендер нарисует ее на экране. Это должно выглядеть похожим на некоторые системы двухмерного рисования, описанные мной в предыдущих статьях «A Web Game in an Hour», «2D Drawing Techniques and Libraries for Web Games» (msdn.microsoft.com/magazine/dn948109) и «2D Game Engines for the Web» (msdn.microsoft.com/magazine/dn973016). Теперь нужно добавить некоторые элементы к сцене.
Геометрия
Почти вся трехмерная графика состоит из полигонов (многоугольников). Искривленные поверхности вроде мяча аппроксимируются треугольными ячейками (facets). После сборки эти треугольники называются мешем (mesh). Вот как добавить мяч к сцене:
var geometry = new THREE.SphereGeometry(10);
var material = new THREE.BasicMaterial({color: 0xFF0000});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
Этот код создаст большое количество треугольников, представляющих сферу (переменная geometry), простой ярко-красный материал (переменная material) и объект mesh. Затем он добавит меш в сцену.
Треугольник является фундаментальным строительным элементом в 3D-графике. Почему? Я подробно исследую этот вопрос в следующей статье, а пока отмечу лишь две основные причины: с прямыми линиями, образующими треугольник, легко работать и треугольник нельзя разбить на еще более элементарную часть плоскости. Графический процессор (GPU) в вашем компьютере или смартфоне имеет специализированное аппаратное обеспечение, способное быстро преобразовывать фигуры с прямыми линиями в пиксели. Это значительная часть того, что делает возможным рисование высококачественной 3D-графики.
Моделирование
Я могу передать любые геометрические элементы (геометрию) в конструктор three.Mesh. К ней относится, в том числе, сгенерированная геометрия для создания собственных форм или даже данные из файлов. В случае игры Ping я предпочел бы 3D-модели каждого из игроков. Поэтому для данного упражнения я позволил себе создать геометрию в программе трехмерного моделирования. Использовать модель вместо сферы на удивление просто, поскольку three.js предоставляет средство загрузки как раз для этой цели:
var jsonLoader = new THREE.JSONLoader();
jsonLoader.load('tank1.json', function (geometry) {
var material = new THREE.BasicMaterial({color: 0xFF0000});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
});
Камера
Камера представляет точку обзора сцены. Она хранит позицию и угол обзора в игре. Еще важнее, что камера представляет то, как сцена становится плоской.
В моем примере камера позиционировалась вниз и вправо. Конечное изображение появлялось как вид с этого направления. Однако при использовании этого метода проекции независимо от того, насколько далеки объекты, они будут оставаться одного и того же размера в конечном изображении. Это называется ортогональной проекцией (orthographic projection). Она зачастую полезна в играх с нереалистичными углами обзора вроде игр, симулирующих жизнь города. Мне же надо добиться того, чтобы объекты становились меньше по мере их отдаления.
Знакомьтесь с перспективной проекцией (perspective projection), или просто перспективой: она имитирует поле обзора камеры как пирамиды, расширяющейся из линзы. Когда позиции преобразуются в экранные, они вычисляются с учетом их относительных расстояний до боков пирамиды. Используя эту модель, где объекты отдаляются с расстоянием, вы добьетесь того, чтобы они уменьшались, как в реальной жизни.
К счастью, вам не придется самостоятельно выполнять это преобразование, потому что three.js делает это за вас и имеет объект, который представляет камеру в сцене (кстати, совсем не сложно добавить еще одну камеру):
var camera = new THREE.PerspectiveCamera(
75, window.innerWidth/window.innerHeight, 0.1, 1000 );
Почти вся трехмерная графика состоит из полигонов.
Первый аргумент — это поле обзора, которое определяет, насколько большое угловое пространство попадает в поле зрения в горизонтальной плоскости. Второй аргумент — отношение ширины и высоты экрана; этот аргумент необходим, чтобы объекты не расплющивались, поскольку экран не является квадратным. Последние два аргумента определяют ближайшее и самое дальнее расстояния, которые должны отображаться. Все, что находится ближе или дальше этих границ, не рисуется. Теперь я нахожусь в точке, от которой можно нарисовать сцену. Давайте сместим камеру немного назад, чтобы видеть всю сцену и начать рисование:
camera.position.z = 50;
renderer.render(scene, camera);
Материалы и источники света
Я помещу мяч на игровую площадку, от стенок которой он будет отскакивать:
var room = new THREE.BoxGeometry( 50, 30, 100 );
var material = new THREE.MeshPhongMaterial({
side: THREE.BackSide,
map: THREE.ImageUtils.loadTexture('arena.png')
});
var model = new THREE.Mesh(room, material);
model.position.y = 15;
scene.add(model);
Я делаю несколько большее простого создания геометрии коробки (box geometry). Я также создаю материал. Материал — это определение того, как объект с данным материалом должен отражать свет в сцене. От этого зависит ее общий внешний вид. В данном случае я выбираю материал Phong, который является хорошим выбором по умолчанию для блестящих объектов. Я также добавляю текстуру для коробки, что легко делается с помощью функции loadTexture из three.js.
Еще один достойный внимания аспект этого кода — строка «side: THREE.BackSide». Это указывает three.js рисовать только внутренние стороны поверхностей коробки, игнорируя внешние стороны. Это дает место для скачущего мяча, а не сплошную коробку, парящую в пространстве.
Если бы я рисовал сцену сейчас, игровая площадка оказалась бы невидимой. Она была бы просто закрашена черным цветом. Дело в том, что материалы определяют, как свет отражается от объектов, а у меня пока нет освещения в сцене. Three.js упрощает добавление освещения в сцену, как показано ниже:
this.lights = [];
this.lights[0] = new THREE.PointLight( 0x888888, 1, 300 );
this.lights[0].position.set( 0, 10, 40 );
scene.add( this.lights[0] );
this.lights[1] = new THREE.PointLight( 0x888888, 1, 300 );
this.lights[1].position.set( 0, 20, -40 );
scene.add( this.lights[1] );
Теперь, если я рисую сцену, игровая площадка визуализируется должным образом. Чтобы улучшить вид, я задам позицию камеры так, чтобы она смотрела в бок площадки:
camera.up.copy(new THREE.Vector3(0,1,0));
camera.position.copy(new THREE.Vector3(0,17, -80));
camera.lookAt(new THREE.Vector3(0,0,40));
В первой строке настраивается переменная up, которая просто сообщает камере, какая сторона является верхом. Функция lookAt делает именно то, что и подразумевает ее название: она направляет камеру на заданную позицию.
Создание 3D-игры
Теперь, когда игра перешла в три измерения, доделать остальное довольно легко. Однако эта реализация будет содержать больше кода, чем предыдущие, поскольку игра содержит трехмерные объекты вместо двухмерных. Поэтому я разобью код на отдельные файлы, чтобы было легче добавлять дополнительный код.
Я также перейду от определений объектов в стиле JavaScript к более традиционной модели конструкторов. Для демонстрации этого я обернул коробку игровой площадки и источников света в объект и поместил его в один файл, показанный на рис. 3.
Рис. 3. Объект Arena
function Arena(scene) {
var room = new THREE.BoxGeometry( 50, 30, 100 );
var material = new THREE.MeshPhongMaterial({
side: THREE.BackSide,
map: THREE.ImageUtils.loadTexture('arena.png')
});
var model = new THREE.Mesh(room, material);
model.position.y = 15;
scene.add(model);
this.lights = [];
this.lights[0]= new THREE.PointLight( 0x888888, 1, 300 );
this.lights[0].position.set( 0, 10, 40 );
scene.add( this.lights[0] );
this.lights[1]= new THREE.PointLight( 0x888888, 1, 300 );
this.lights[1].position.set( 0, 20, -40 );
scene.add( this.lights[1] );
}
Если мне нужно создать Arena, можно создать новый объект, используя его функцию-конструктор:
var arena = new Arena(scene);
Затем я создам объект Ball, который может отскакивать от стенок игровой площадки. Я знаю, как сделать мяч красным в three.js, поэтому оберну и этот код в объект:
function Ball(scene) {
var mesh = new THREE.SphereGeometry(1.5, 10, 10);
var material = new THREE.MeshPhongMaterial({
color: 0xff0000,
specular: 0x333333
});
var _model = new THREE.Mesh(mesh, material);
_model.position.y = 10;
scene.add(_model);
}
Теперь я определю базовую физику отскакивания мяча, добавив функцию к объекту мяча, как показано на рис. 4.
Рис. 4. Функция update объекта Ball
// Создаем закрытую переменную класса
// и присваиваем ей некое начальное значение
var _velocity = new THREE.Vector3(40,0,40);
this.update = function(t) {
// Применяем к мячу гравитацию
_velocity.y -= 25 * t;
// Перемещаем мяч согласно его скорости
var offset = _velocity.clone()
.multiplyScalar(t);
_model.position.add(offset);
// Теперь заставляем его отскакивать от стенок и пола.
if (_model.position.y - 1.5 <= 0) {
_model.position.y = 1.5;
_velocity.y *= -1;
}
if (_model.position.x - 1.5 <= -25) {
_model.position.x = -23.5;
_velocity.x *= -1;
}
if (_model.position.x + 1.5 >= 25) {
_model.position.x = 23.5;
_velocity.x *= -1;
}
}
Three.js требует выполнять рендеринг сцены при каждом использовании requestAnimationFrame. Этот шаблон должен быть вам знаком:
var ball = new Ball(scene);
var Arena = new Arena(scene);
var render = function (time) {
var t = (time - lastTime) / 1000.0;
lastTime = time;
ball.update(t);
renderer.render(scene, camera);
requestAnimationFrame( render );
}
requestAnimationFrame(render);
Не переключайтесь
Теперь у меня есть игровая площадка с освещением, корректно расположенная камера и мяч, скачущий по площадке. Это все, о чем я хотел рассказать в этой статье. В следующей статье я объясню, как работает 3D-проекция, позволив вам прицеливаться мышью. Я также подробнее расскажу о текстурах и создам плавные анимации, используя мощную библиотеку tween.js. В последней из этих трех статей я загляну «под капот» three.js и покажу, как она на самом деле рисует столь высококачественную графику.
Майкл Онеппо (Michael Oneppo) — креативный технолог и бывший менеджер программ в группе Microsoft Direct3D. В последнее время работает в качестве главного технического директора в технологической некоммерческой компании Library For All и ведет исследования по программе NYU Interactive Telecommunications Program для получения степени магистра.
Выражаю благодарность за рецензирование статьи эксперту Мохамеду Амину Ибрагиму (Mohamed Ameen Ibrahim).