Поделиться через



Август 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.

Простая 3D-сцена
Рис. 1. Простая 3D-сцена

В этой сцене ось Z направлена вверх и в обратном направлении. Если бы я хотел действительно увидеть ее на экране, я мог бы просто удалить Z-информацию из каждого объекта — это простой и допустимый способ проецирования 3D-сцены, как показано на рис. 2.

Расплющенная 3D-сцена
Рис. 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).