Готовый перевод Adobe Flash 11 Stage3D (Molehill) game programming (Chapter 6 - 10) / Программирование игр в Adobe Flash 11 Stage3D (Molehill) (Глава 6 - 10): Привлекательное в изобилии
Вы достигли уровня 8.
Следующий шаг в нашем квесте графического великолепия это реализовать кое-что привлекательное.
Система частиц и спецэффекты необходимые для добавления в вашу игру визуальные составляющие.
Вещи становятся действительно захватывающими сейчас. Вы возможно можете уже увидеть свет в конце туннеля: наша игра принимает форму и мы уже изучили так много. Сейчас мы уже достигли некоторого основнго геймплея - движение, анимация, GUI heads-up дисплея, таймеры и классы ввода - следующий шаг это сделать это хорошо выглядящим.
В этой главе мы реализуем систему, способную производить огромное количество спецэффектов. Есть серьёзные основания принять во внимание, помимо всего прочего, что мы не хотим, чтобы наши спецэффекты разрушили нашу гладкую шелковистую частоту кадров.
Наш текущий квест.
Принимая во внимание оптимизацию и абстрактную способность к повторному использованию, давайте создадим менеджер системы частиц, способный к рендерингу безумного числа частиц и, в то же время, имеющий минимальный эффект на частоту кадров. Мы хотим быть способны выводить всё от взрывов, дыма, искр, ударных волн, брызг крови, бликов, брызг воды или лавы и до пылинок парящих в воздухе.
Что мы хотим это быть способными отображать МАССУ частиц с прекрасной частотой кадров:
Прежде чем мы погрузимся, мы должны принять к сведению некоторые цели проектирования для нашей фантастической системы частиц.
Проектирование для производительности.
Многие авторы движков Flash 3D игрв последнее время имеют проблемы с хорошей производительностью системы частиц. Причина в том, что наш первый инстинкт это просто рисовать тысячи изображений спрайтов по отдельности. Конечно,это приводит к тому, что есть избыток в вычислениях трансформаций матриц и перерисовке состояния, которое вызывает каждый рисунок, и единственный способ как мы можем достичь большого числа частиц во Flash это группировать их большими кусками. Ещё лучше, если мы сможем избежать выполнения анимационных симуляций на CPU и возложим все вычисления на GPU, используя AGAL. Наши движки частиц могут приносить пользу фактически без использования CPU.
Мы хотим систему частиц, которая позволяет мегасложные системы частиц почти бесконечной пыльцы. Возможное решение это создать специализированный вершинный буфер для всей системы частиц.
С этой техникой вся система частиц будет управляться на GPU фактически без избыточного рендеринга для анимации гигантской системы частиц. Почему? Целая вещь рендерится за один вызов отрисовки. По-существу мы "предрассчитываем" целую анимацию для ваших взрывов и других эффектов. Не волнуйтесь о каждом отдельном спрайте во взрыве, вы просто рисуете целую группу в одно время и поручаете GPU делать всю тяжёлую работу.
Используя эту технику мы сможем реализовать полностью анимированную в GPU систему частиц (60fps с 40000 частиц, где каждая движется с различным направлением, поворотом и увеличивается во время затухания), которая использует AGAL для всех симуляций - не CPU.
Проектирование для повторного использования
Мы не хотим жёстко кодировать рендеринг наших спецэффектов для конкретного вида эффектов. Поэтому мы нацелимся на способности отправлять системе любой вид мешей: от простых плоских спрайтов, состоящих из базовых четырёхугольников, до целых групп полигонов, которые составляют сложный взрыв.
Другая форма повторного использования это способность использовать ранее созданные частицы снова и снова. Мы также не хотим увязнуть в CPU и поглощать массу RAM каждый кадр, создавая и разрушая тысячи мешей снова и снова. Делая так, мы убьём частоту кадров, потому что время, требующееся для загрузки данных меша (или даже создания простых AS3 объектов) добавляется - если мы собираемся порождать частицы снова и снова во время игры, нет причин непрерывно инициализировать их и затем разрушать, когда они пропадают.
Поэтому помимо выполнения всех симуляций отдельно на GPU, другой техникой оптимизации, используемой здесь, является создание "бассейна частиц", так что эти частицы повторно используются, если не активны. Это позволяет избежать проблем со сборщиком мусора, которые приводят к сбою частоты кадров: мы не "порождаем" новые частицы каждый кадр, когда возникает новый взрыв, мы просто повторно используем старые и только создаём новые, если нет доступных неактивных. Результат этого - частота кадров остаётся в районе 60 fps, создаём ли мы сорок тысяч частиц на экране или вовсе ни одной.
Анимация с использованием AGAL.
AGAL и место установки для техники, описанной ранее относительно упрощено. Мы создаём два вершинных буфера. Один содерджит начальные позиции и другие конечные позиции предвычисляемые для каждой вершины. Например, вместо создания тысяч GameEntities, создаём один меш из 10000 полигонов со следующими данными вершинного буфера: x1,y1,z1,x2,y2,z2. Вместо того, чтобы передвигать каждую частицу через математические матрицы AS3, движение спрайта предвычисляется и само сохраняется в вершинном буфере.
Однажды создав эту частицу "группы геометрии" мы можем отправить дельта значение как констатный регистр вершинного буфера, который варьируется от 0 до 1 каждый раз. В каждом кадре, когда мы рендерим меш, у нас будет ваша вершинная программа AGAL , которая интерполирует позицию x, y, z каждой вершины от начальной до конечной позиции. Каждая вершина будет гладко двигаться от первой ко второй позиции.
Предрассчитывая движения каждой частицы один раз и затем просто получая AGAL, чтобы плавно переместить из одного места в другое, мы можем симулировать гравитацию, ветер, увеличение размера таких вещей как рассеивание дыма в воздухе и другие.
Для того, чтобы достичь великолепной производительности во время геймплея, мы не хотим делать никаких сложных вычислений в AS3. Вместо этого, однажды заполнив два вершинных буфера, описанных ранее, всё, что мы должны делать каждый кадр это инструктировать Stage3D сколько времени прошлою Ранее, когда мы рендерили наш меш частиц, мы отправляли это значение в Stage3D для использования в шейдере.
Ключевое дизайнерское решение здесь - НЕ рендерить или симулировать каждую частицу отдельно. В зависимости от вычислительных возможностей вашего компьютера (и, в частности, скорости заполнения вашей видеокарты) вполне осуществимо, что ваша игра сможет похвастаться сценой, содержащей 100000+ частиц и, в то же время, всё ещё выполняющейся при 60fps - с возможностью сохранить все ваши другие модели, т.к. мы не используем CPU для всякой тяжёлой работы.
Для того, чтобы успешно завершить этот квест, мы должны выполнить следующее:
Создать основной класс сущностей частиц
Запрограммировать AGAL для ключевых анимаций вершин
Закодить менеджер системы частиц, чтобы контролировать множество частиц
Спроектировать некоторые хорошо выглядящие арты
Объединить систему частиц с нашей игрой
Время обновить приятности нашей игры. Эта эффективная технология "пакетной системы частиц в GPU" даст вам шик, необходимый для некоторых превосходных скриншотов и внутриигровой последовательности действий. Давайте приступим!
Основной класс системы частиц.
Первый шаг в добавлении красивостей к нашей игре это дать определение что же такое частицы. Постепенно наша игра породит сотни для использования во всех видах спецэффектах, так что давайте сделаем это абстрактным и сможем использовать многочисленные виды частиц - от брызг воды до искр, которые летят от скрещивающихся мечей.
Одна частица в нашей системе спецэффектов может быть отдельным полигоном или предварительно подготовленным "пакетом" множества полигонов. Так как мы хотим иметь множество многоугольников с минимальным расходом частоты кадров, мы позаботимся о всех эффектах (таких как взрыв) как единственной сущности, даже хотя меш, который это использует будет состоять из сотен тысяч полигонов.
Мы хотим, чтобы наш класс сущностей частиц поддерживал данным меша так же как текстура или шейдер. Это звучит подозрительно похоже на класс Stage3dEntity, который мы уже запрограммировали.
Хорошие новсти это то, что мы только должны добавить несколько незначительных дополнений в этот класс сущностей, так что вместо создания новых впечатляющих видов AS3 объектов с нуля, мы просто расширим основной класс сущностей и добавим несколько дополнительных функций и переменных.
Время для действий - расширяем класс сущностей для частиц.
Создадим новый файл в вашей папке исходного кода и назовём его Stage3dParticle.as. Импортируем основные функции, которые нам нужны и просто расширим базовый класс следующим образом:
//игровой класс частиц версия 1.0
//
package
{
import com.adobe.utils.*;
import flash.display.Stage3D;
import flash.display3D.Context3D;
import flash.display3D.Context3DProgramType;
import flash.display3D.Context3DTriangleFace;
import flash.display3D.Context3DVertexBufferFormat;
import flash.display3D.IndexBuffer3D;
import flash.display3D.Program3D;
import flash.display3D.VertexBuffer3D;
import flash.display3D.*;
import flash.display3D.textures.*;
import flash.geom.Matrix;
import flash.geom.Matrix3D;
import flash.geom.Vector3D;
public class Stage3dParticle extends Stage3dEntity
{
Что же произошло?
Последняя строка предыдущего кода инструктирует Flash "расширить" или взять все функции которые были реализованы в нашем классе Stage3dEntity и предполагает, что они такие же в новом классе. В этом случае наш класс частиц незамедлительно имеет все вспомогательные функции во всём их ассортименте, которые мы так тяжело разработали ранее.
Это использование наследования классов помогает сэкономить много времени, позволяя повторно использовать код. Это сохраняет базовую функциональность в одном месте. Если мы обновляем этот базовый класс или исправляем ошибку, тогда все классы, которые расширяют его, также автоматически обновляются.
Время для действий - добавляем свойства частиц.
Теперь мы хотим дать нашему классу несколько расширенных свойств, относящихся к частицам. Мы сделаем это следующим образом:
public var active:Boolean = true;
public var age:uint = 0;
public var ageMax:uint = 1000;
public var stepCounter:uint = 0;
private var mesh2:Stage3dObjParser;
private var ageScale:Vector.<Number> =
new Vector.<Number>([1, 0, 1, 1]);
private var rgbaScale:Vector.<Number> =
new Vector.<Number>([1, 1, 1, 1]);
private var startSize:Number = 0;
private var endSize:Number = 1;
// мы хотим компилировать шейдеры только один раз
private static var particleshader1mesh:Program3D = null;
private static var particleshader2mesh:Program3D = null;
Что же произошло?
Эти свойства будут использованы, чтобы симулировать нашу систему частиц. Отслеживая возраст, мы можем вычислять подходящий размер и цвет с течением времени, так что взрыв, например, начинается маленьким и увеличивается в размере постепенно затухая. Время хранится в миллисекундах, так что по умолчанию будет создана частица, которая существует ровно одну секунду.
Время для действий - программируем конструктор класса частиц.
Теперь, когда мы определили переменные, которые нам нужны, давайте напишем новую функцию конструктора класса. Мы не хотим использовать ту, которая перешла к нам от нашего оригинального класса Stage3dEntity, так что мы используем ключевое слово "override", чтобы заменить её нашей новой версией. Хотя это похоже на основную сущность, наши частицы могут иметь необязательный второй меш, который используется в покадровой анимации позиций вершин. Мы также не должны передавать ничего шейдеру, так как мы определим особый AGAL шейдер позже.
//Класс конструктора - второй меш определяет
//конечную позицию каждой вершины в первом
public function Stage3dParticle(
mydata:Class = null,
mycontext:Context3D = null,
mytexture:Texture = null,
mydata2:Class = null
)
{
transform = new Matrix3D();
context = mycontext;
texture = mytexture;
// используем шейдер специально спроектированный для частиц
// это версия для двух кадров: интерполяция из одного
// меша в другой с течением времени
if (context && mydata2) initParticleShader(true);
// только один меш определён: используем простой шейдер
else if (context) initParticleShader(false);
if (mydata && context)
{
mesh = new Stage3dObjParser(
mydata, context, 1, true, true);
polycount = mesh.indexBufferCount;
trace("Mesh has " + polycount + " polygons.");
}
// парсим второй меш
if (mydata2 && context)
mesh2 = new Stage3dObjParser(
mydata2, context, 1, true, true);
// решим рендеринга по умолчанию, подходящий для частиц
blendSrc = Context3DBlendFactor.ONE;
blendDst = Context3DBlendFactor.ONE;
cullingMode = Context3DTriangleFace.NONE;
depthTestMode = Context3DCompareMode.ALWAYS;
depthTest = false;
}
Что же произошло?
Этот конструктор просто устанавливает сущность частиц, чтобы она рендерилась с правильным режимом наложения. Если мы отправляем второй меш в параметры, тогда меш также парсится и готов к использованию в нашем шейдере. В зависимости от того, использует ли частица отдельный меш или интерполирует форму из неё после этого, создаётся подходящий шейдер.
Время для действий - клонируем частицы.
Как только нам нужно скомпилировать AGAL шейдер или обновить геометрию меша в Stage3D один раз для каждого вида частиц, мы избегаем траты RAM создавая специальную функцию клонирования:
public function cloneparticle():Stage3dParticle
{
var myclone:Stage3dParticle = new Stage3dParticle();
updateTransformFromValues();
myclone.transform = this.transform.clone();
myclone.mesh = this.mesh;
myclone.texture = this.texture;
myclone.shader = this.shader;
myclone.vertexBuffer = this.vertexBuffer;
myclone.indexBuffer = this.indexBuffer;
myclone.context = this.context;
myclone.updateValuesFromTransform();
myclone.mesh2 = this.mesh2;
myclone.startSize = this.startSize;
myclone.endSize = this.endSize;
myclone.polycount = this.polycount;
return myclone;
}
Что же произошло?
Всё что делает эта функция - это копирует переменные, необходимые для новой копии текущей частицы. Это одна из различных оптимизаций, которая будет позволять нашей игре рендерить МАССУ частиц, используя минимальные системные ресурсы.
Время для действий - генерируем числа, используемые для анимации.
Следующие две функции просто удобные алгоритмы, которые вы можете захотеть использовать для симуляции частиц. Они плавно интерполирую значение используя функцию синуса, так, что оно "колеблется" и завершается. Это функция очень удобна как ваши проектные частицы, которые должны качаться вверх и вниз или блекнуть и постепенно плавно исчезать.
private var twoPi:Number = 2*Math.PI;
//возвращает число с плавающей запятой из -amp to +amp в колебаниях в секунду
private function wobble(
ms:Number = 0, amp:Number = 1, spd:Number = 1):Number
{
var val:Number;
val = amp*Math.sin((ms/1000)*spd*twoPi);
return val;
}
// returns a float that oscillates from 0..1..0 each second
private function wobble010(ms:Number):Number
{
var retval:Number;
retval = wobble(ms-250, 0.5, 1.0) + 0.5;
return retval;
}
Что же произошло?
Что же эти функций "колебаний" делают? Они используются для того, чтобы генерировать числа, которые плавно меняются от нуля до единицы (и возможно возвращаются обратно снова), основываясь на том, как много времени прошло. Они отправляются в AGAL код в нашем шейдере частиц, чтобы наши частицы появлялись и исчезали и заставляя их увеличиваться в размерах.
Время для действий - симулируем частицы.
Следующий шаг - это запрограммировать пошаговую функцию. Это функция будет в итоге вызываться один раз каждый отдельный кадр, для каждой отдельной частицы вашим классом менеджера системы частиц. Поэтому для этой функции важно быть настолько простой, насколько это возможно.
public function step(ms:uint):void
{
stepCounter++;
age += ms;
if (age >= ageMax)
{
//trace("Particle died (" + age + "ms)");
active = false;
return;
}
// основываясь на возрасте, меняем масштаб, начиная с pos (1..0)
ageScale[0] = 1 - (age / ageMax);
// основываясь на возрасте, меняем масштаб, начиная с pos (0..1)
ageScale[1] = age / ageMax;
// основываясь на возрасте: переходим от 0..1..0
ageScale[2] = wobble010(age);
// убеждаемся, что это внутри валидного интервала
if (ageScale[0] < 0) ageScale[0] = 0;
if (ageScale[0] > 1) ageScale[0] = 1;
if (ageScale[1] < 0) ageScale[1] = 0;
if (ageScale[1] > 1) ageScale[1] = 1;
if (ageScale[2] < 0) ageScale[2] = 0;
if (ageScale[2] > 1) ageScale[2] = 1;
//прозрачность появляется и исчезает
rgbaScale[0] = ageScale[0];
rgbaScale[1] = ageScale[0];
rgbaScale[2] = ageScale[0];
rgbaScale[3] = ageScale[2];
}
Что же произошло?
Используя время, прошедшее с предыдущего кадра, эта функция использует функции колебаний, чтобы сгенерировать список чисел, которые постепенно идут от нуля до единицы или от единицы до нуля, в зависимости от того, что требуется.
Во-первых, проверяется, чтобы увидеть, если возраст частицы вышел за пределы её желаемого времени жизни. Если так, частица более не нуждается в обновлении или рендеринге. Если она наоборот ещё активна, мы вычисляем возраст, масштаб и rgba значения, которые будут использованы нашим шейдерам, чтобы выполнить анимацию.
Время для действий - возрождаем частицы.
Как упоминалось ранее, мы хотим создавать совершенно новые частицы только когда нет ни одной неактивных частиц, которые могут быть использованы повторно. Это то, как мы способны получить так много полигонов на экране с такой великолепной производительностью. Избегая создания и разрушения новых объектов каждый кадр, мы сохраняем требования памяти постоянными во время игрового цикла.
public function respawn(
pos:Matrix3D, maxage:uint = 1000,
scale1:Number = 0, scale2:Number = 50):void
{
age = 0;
stepCounter = 0;
ageMax = maxage;
transform = pos.clone();
updateValuesFromTransform();
rotationDegreesX = 180; // point "down"
// начинаем случайную ориентацию каждый раз
rotationDegreesY = Math.random() * 360 - 180;
updateTransformFromValues();
ageScale[0] = 1;
ageScale[1] = 0;
ageScale[2] = 0;
ageScale[3] = 1;
rgbaScale[0] = 1;
rgbaScale[1] = 1;
rgbaScale[2] = 1;
rgbaScale[3] = 1;
startSize = scale1;
endSize = scale2;
active = true;
//trace("Respawned particle at " + posString());
}
Что же произошло?
Функции возрождения используются нашим предстоящим менеджером системы частиц чтобы сбросить требуемые переменные так, чтобы существующие частицы были готовы к "новой жизни". Всякий раз, когда наши игры должны создавать, например, новый взрыв, мы просто должны инструктировать существующую частицу начинать анимацию с нового местоположения. Мы можем также корректировать размер или продолжительность жизни нашей частицы, если необходимо, так что мы можем делать такие вещи как создавать взрывы больше и меньше в зависимости от причиняемого урона.
Одно важное замечание: мы поворачиваем наш меш взрыва "по направлению вниз" в этом примере потому что данные примера в нашем втором .OBJ файле имеют каждую частицу в слегка высоком положении. В зависимости от вашего исходного арта, вы можете не захотеть включить эту строчку. Вы могли бы также захотеть усилить этот алгоритм, чтобы он был способен отключить, например, случайную ориентацию, которая используется здесь, чтобы заставить каждый взрыв выглядеть более отличным от предыдущего.
Переменные возраста, масштаба и rgba специализируются со значениями, которые имеют смысл для каждого первого кадра нашего шейдера частиц. Мы будем изменять эти значения каждый кадр для того, чтобы меш исчезал или выростал, отправляя их в наш AGAL шейдер для использования как вершинные и фрагментные константы.
Время для действий - рендерим частицы.
Точно так же как мы расширили функцию конструктора по умолчанию класса Stage3dEntity, мы хотим также написать нашу собственную функцию рендеринга частиц. Перезапишем функцию рендеринга следующим образом:
//оптимизация: повторно используем те же самые временные матрицы
override public function render(
view:Matrix3D,
projection:Matrix3D,
statechanged:Boolean = true):void
{
// только рендеринг, если это установлено
if (!active) return;
if (!mesh) return;
if (!context) return;
if (!shader) return;
if (!texture) return;
// становится больше каждый раз
scaleXYZ = startSize +
((endSize - startSize) * ageScale[1]);
//сбрасываем нашу матрицу
_rendermatrix.identity();
_rendermatrix.append(transform);
if (following) _rendermatrix.append(following.transform);
_rendermatrix.append(view);
_rendermatrix.append(projection);
// Устанавливаем вершиный програмный регистр vc0
// в нашу матрицу model view projection
context.setProgramConstantsFromMatrix(
Context3DProgramType.VERTEX, 0, _rendermatrix, true);
Мы начинаем с первого обновления масштаба нашего меша, чтобы увеличивать или сокращать нашу частицу в зависимости от значений, которые мы отправили в функцию возрождения. Простое масштабирование обновляет нашу матрицу трансформации используя переменную ageScale. Затем она смешивается с матрицей обзора и проекции так же, как мы делали во всех предыдущих примерах визуализации.
//Устанавливаем вершинный программный регистр vc4
//в наше время и масштаб из (0..1)
//используется, чтобы интерполировать позицию вершины с течением времени
context.setProgramConstantsFromVector(
Context3DProgramType.VERTEX, 4, ageScale);
// Устанавливаем фрагментный програмный регистр fc0
// в наше время и масштаб из (0..1)
// используется, чтоб интерполировать прозрачность с течением времени
context.setProgramConstantsFromVector(
Context3DProgramType.FRAGMENT, 0, rgbaScale);
Затем мы отправляем наши интерполированные значения в шейдер. Заполняя вершинные программные константы с ageScale и фрагментную программную константу с rgbaScale, наш Stage3D шейдер сможет сделать отсаток работы за нас. Помните, что те две переменные содержат плавно интерполированный список чисел (четыре в каждом), которые идут от 1 до 0 или от 0 до 1 с течением времени, как это необходимо. Функция step() это функция, где эти значения обновляются в каждом кадре.
//Устанавливаем программу AGAL
context.setProgram(shader);
// Устанавливаем фрагментный программный регистрr ts0 для текстуры
context.setTextureAt(0,texture);
// начинам с позиции (va0)
context.setVertexBufferAt(0, mesh.positionsBuffer,
0, Context3DVertexBufferFormat.FLOAT_3);
// текстурные координаты (va1)
context.setVertexBufferAt(1, mesh.uvBuffer,
0, Context3DVertexBufferFormat.FLOAT_2);
// конечная позиция (va2)
if (mesh2)
{
context.setVertexBufferAt(2, mesh2.positionsBuffer,
0, Context3DVertexBufferFormat.FLOAT_3);
}
// устанавливаем состояние рендеринга
context.setBlendFactors(blendSrc, blendDst);
context.setDepthTest(depthTest,depthTestMode);
context.setCulling(cullingMode);
// рендерим её
context.drawTriangles(mesh.indexBuffer,
0, mesh.indexBufferCount);
} // конец функции рендеринга
Что же произошло?
Функция рендеринга частиц посылает числа, основанные на том, сколько времени прошло в нашем специальном шейдере частиц, так что меш будет рендериться с правильным размером и прозрачностью. Эти числа вычисляются в функции step() и сохраняются в AGAL регистрах для использования в наших вершинной и фрагментной программах.
Наконец, мы инструктируем Flash, какой вершинный буфер и шейдер использовать, устанавливаем состояние рендеринга так, чтобы он использовал свойство прозрачности ( в этом примере, аддитивное освещение сцены) и рендерим наш меш.
Есть только одна недостающая функция в нашем классе частиц - AGAL шейдер - которая заслуживает боле детального обсуждения.
Покадровая анимация вершинного шейдера.
Последний штрих, требующийся для того, чтобы завершить наш класс частиц, это запрограммировать шейдер, который делает всю тяжёлую работу во время нашего игрового цикла. В функции конструктора нашего класса частиц мы вызываем эту функцию initParticleShader и она возвращает шейдер, который использует либо два меша ( для покадровой анимации), либо один, если это всё, что требуется.
Как упоминалось в предыдущих главах, Stage3D шейдер сделан из вершинной и фрагментной программы. Бывшие обработчики положений, UV координат и прочего для каждой вершины в вашем меше, пока более поздний решает какой цвет и прозрачность имеет каждый пиксель на экране, когда ваш меш рендерится.
Так как мы хотим заставить GPU вашей видеократы делать всю матричную математику для движения наших частиц (освобождаем нашу CPU, чтобы та думала о других вещах), мы должны написать вершинную программу AGAL, которая может передвигать вершины вашего меша с течением времени.
Один способ сделать это - сохранять конечные позиции каждой вершины рядом с начальными позициями и просто интерполировать одно значение в другое с течением времени. Простой путь установить это - ваять, например, взрыв в любом приложении 3D моделирования, которое вы предпочитаете. Затем, после экспортирования первой версии нашего меша как .OBJ файла для использования в нашей игре, поворачиваем и передвигаем форму и экспортируем второй меш.
Это очень упрощённая форма покадровой анимации. Кадр 1 вашей анимации - это первый меш, и кадр 2 - второй. Шейдер меняет форму нашего меша от первого ко второму с течением времени.
Эта техника двух мешей необязательна: если вы хотите, вы можете пропустить только один меш в ваш конструктор и просто масштабировать весь меш как целое (используя параметры функции возрождения) пока он затухает. Однако, для дополнительного движения ( и так, чтобы каждая частица в вашем эффекте могла двигаться независимо, а не как целое) этот двухкадровый подход легко достижими и приводит к великолепным визуальным эффектам.
Время для действий - создаём покадровую вершинную программу частиц.
Добавьте следующее в самый конец вашего класса Stage3dParticle.
private function initParticleShader(twomodels:Boolean=false):void
{
var vertexShader:AGALMiniAssembler =
new AGALMiniAssembler();
var fragmentShader:AGALMiniAssembler
= new AGALMiniAssembler();
if (twomodels)
{
if (particleshader2mesh)
{ // уже скомпилировано ранее?
shader = particleshader2mesh;
return;
}
trace("Compiling the TWO FRAME particle shader...");
vertexShader.assemble
(
Context3DProgramType.VERTEX,
// масштабируем начальную позицию
"mul vt0, va0, vc4.xxxx\n" +
// масштабируем конечную позицию
"mul vt1, va2, vc4.yyyy\n" +
// интерполируем две позиции
"add vt2, vt0, vt1\n" +
// 4x4 матричное умножение, чтобы получить угол камеры
"m44 op, vt2, vc0\n" +
// рассказываем фрагментному шейдеру о UV
"mov v1, va1"
);
}
Что же произошло?
Предыдущий AGAL код используется, чтобы интерполировать позиции вершин меша 1 на позиции вершин меша 2. В нашей функции рендеринга мы используем функцию setVertexBufferAt, чтобы va0 содержал первый вершинный буфер и va2 как второй. Мы также отправляем переменную ageScale, чтобы список значений, который меняется от 0 до 1 с течением времени, в Stage3D, используя setProgramConstantsFromVector. Эти четыре числа из списка теперь располагаются в вершинном константном регистре vc4, готовые к использованию.
Для каждой вершины, предыдущий шейдер умножает позицию xyz первого меша (сохранённую в va0) на первое число, сохранённое в vc4 (которое в нашем примере начинается с 1 и постепенно идёт к 0). Поэтому первая строка AGAL выше изменяет местоположение и сохраняет это значение в временный регистр vt0.
Вторая строка AGAL выше делает то же самое для меша "кадра два", за исключением того, что умножает его на второе число в ageScale( y-компонент vc4) и сохраняет его в временном регистре vt1.
Наконец эти два изменённых местоположения (vt0 и vt1) добавляются и сохраняются в vt2, который теперь интерполированное значение. В зависимости от того, как много времени прошло, оно представляет собой точку где-то между первой и второй позицией вершины. Эта новое место положение - матрица, умноженная на model view projection-матрицу тем же способом, который имели все предыдущие вершинные программы, таким образом, угол нашей камеры берётся из счётчика. Наконец, мы посылаем текстурные UV координаты для каждой вершины в изменяющийся регистр v1 для использования в нашем фрагментном шейдере.
Время для действий - создаём статичную вершинную программу частиц.
Продолжим с этой функции, добавив следующее:
if (particleshader1mesh)
{
// уже скомпилировано ранее?
shader = particleshader1mesh;
return;
}
trace("Compiling the ONE FRAME particle shader...");
vertexShader.assemble
(
Context3DProgramType.VERTEX,
// получаем позицию вершины, умноженную на угол камеры
"m44 op, va0, vc0\n" +
// рассказываем фрагментному шейдеру о UV
"mov v1, va1"
);
}
Что же произошло?
В предыдущем коде, если мы используем частицу одного меша (без покадровой анимации), тогда AGAL для вершинного шейдера чрезвычайно прост: он просто инструктирует Stage3D рассчитать угол камеры и сохраняет правильную позицию вершины в op (output position) регистре.
Время для действий - создаём фрагментную программу частиц.
Теперь, когда мы запрограммировали вершинную программу AGAL, которую можно плавно интерполировать из одного положения в другое с течением времени, мы можем сделать что-то похожее в фрагментной программе, чтобы генерировать подходящие цвета пикселов на экране. Продолжим с функции initParticleShader:
//текстурируем используя UV координаты
fragmentShader.assemble
(
Context3DProgramType.FRAGMENT,
// захватываем цвет текстуры из текстуры 0
// и uv координаты из меняющегося регистра 1
// и сохраняем интерполированное значение в ft0
"tex ft0, v1, fs0 <2d,linear,repeat,miplinear>\n" +
// умножаем на "затухающий" регистр цвета (fc0)
"mul ft0, ft0, fc0\n" +
// перемещаем это значение в результирующий цвет
"mov oc, ft0\n"
);
Что же произошло?
В AGAL выше мы создаём фрагментную программу, которая берёт выборку текстуры, которую определяет наша функция рендеринга как fs0 в подходящем положении основанном на UV текстурных координатах, которые наша вершинная программа отправляет в меняющийся регистр v1. Цвет текстуры в этой точке сохраняется в временном регистре фрагментной программы ft0.
Перед рендерингом, используя это значение, мы хотим быть способны плавно исчезать с течением времени, так чтобы наши взрывы или клубы дыма не выскакивали внезапно, а, вместо этого, исчезали в ничто. В нашей функции рендеринга мы отправляем переменную rgbaScale в Stage3D в fc0, фрагментный константный регистр. Точно также как временная шкала используется в нашей вершинной программе, четыре числа сохраняются в fc0 и будут колебаться от 0 до единицы и снова до нуля - то, что надо для хорошего гладкого затухания.
Во второй строке AGAL выше мы просто умножаем цвет текстуры, который мы получили на первом шаге, на затухающее значение сохранённое в fc0. Затем мы сохраняем конечный, должным образом ослабевший цвет в oc (output color) регистр.
Время для действий - компилируем шейдер частиц.
Теперь, когда мы написали подходящий AGAL для использования в наших частицах, мы просто компилируем их вместе, чтобы сформировать шейдер и загружаем в графическую карту.
//комбинируем шейдеры в программе и загружаем в GPU
shader = context.createProgram();
shader.upload(
vertexShader.agalcode,
fragmentShader.agalcode);
// запоминаем этот шейдер для повторного использования
if (twomodels)
particleshader2mesh = shader;
else
particleshader1mesh = shader;
} // конец функции initParticleShader
} // конец класса
} // конец пакета
Это всё для нашего нового причудливого класса Stage3dParticle! Вы могли бы начать использовать этот класс вместо уже знакомого Stage3dEntity в вашей игре, если вы хотите создавать отдельные уникальные частицы здесь и там. Так как этот класс производный от Stage3dEntity, вы могли бы даже заставить его "следовать" чему-то ещё. Это было бы великолепно, если бы вы захотели добавить "свечение" к фонарном столбу, например.
Этот класс прекрасно подходит для одной частицы, которую предполагается сохранять долгое время в вашей игре. Однако, для временных спецэффектов, таких как клубы дыма или брызги крови, нам нужен менеджер системы частиц, способный повторно обходить списки из сотен частиц и повторно использовать их снова и снова.
Класс менеджера системы частиц.
В дополнение к группированию геометрии, создания анимации и затухания в AGAL, другая великолепная оптимизация используется для достижения хорошей производительности частиц, ничего конкретно не делая с Stage3D API и вместо стандартной системы игровой оптимизации: "бассейн" частиц, который может быть повторно использован.
Второй класс, который нам нужно запрограммировать для того, чтобы заполнить нашу игру приятностями, это желаемый нами, менеджер множества частиц. Эта система будет отслеживать прошедшее время, и будем вызывать этот шаг и функцию рендеринга для всех активных частиц. Где бы нашей игре ни нужно было создать спецэффект какого-либо вида, система частиц сделает эту работу.
По причинам производительности мы не хотим создавать и разрушать объекты во время игрового цикла, так что вместо этого мы повторно просто используем неактивные частицы, везде, где это возможно. Если вся очередь сущностей частиц видна на данный момент, только ТОГДА мы создаём новый объект.
Избегая любых распределений переменных или разрушений во время игры, мы никогда не сталкиваемся с сборщиком мусора. Проблема сборщика мусора - причина торможений в вашей игре и снижает частоту кадров. Они вызываются временными переменными, которые больше не используется и разрушаются Flash. Даже хуже, если вы не минимизируете создание переменных во время гейиплея, вы получите то, что называется "утечка памяти" - рост использования системной RAM, который никогда не кончается.
Когда память компьютера заполняется бесполезным старьём, браузер в конечном счёте падает. Вот почему мы должны быть аккуратно избегать создания слишком большого числа временных переменных, и почему мы бы никогда не хотели программировать систему частиц, которая создаёт новый Stage3dParticle каждый раз, когда мы хотим поместить спецэффект - в итоге там были бы миллионы в памяти, замедляя всё.
Создавая класс, который разумно использует повторно неактивные объекты, которые мы создали ранее, мы сохраняем все те трудности и получаем выгоду очень плавной частоты кадров!
Время для действий - программируем класс менеджера системы частиц.
Этот класс контроллера будет намного проще, чем класс частиц, который мы запрограммировали ранее. Менеджер системы частиц это просто контролирующий механизм, который пытается повторно использовать неактивные частицы где только возможно. Создайте новый файл в вашей папке проекта под именем GameParticlesystem.as и определим наш новый класс следующим образом:
//Игровой менеджер системы частиц версия 1.0
//создаёт бассейн сущностей частиц по требованию
//и повторно использует неактивные где только возможно
//
package
{
import flash.utils.Dictionary;
import flash.geom.Matrix3D;
import flash.geom.Vector3D;
import Stage3dParticle;
public class GameParticlesystem
{
// содержит один источник частиц для каждого вида
private var allKinds:Dictionary;
// содержит много клонированных частиц различного вида
private var allParticles:Dictionary;
// временные переменные - используются часто
private var particle:Stage3dParticle;
private var particleList:Vector.<Stage3dParticle>;
// используется для многих состояний
public var particlesCreated:uint = 0;
public var particlesActive:uint = 0;
public var totalpolycount:uint = 0;
// конструктор класса
public function GameParticlesystem()
{
trace("Particle system created.");
allKinds = new Dictionary();
allParticles = new Dictionary();
}
Что же произошло?
В предыдущем коде мы просто определяем наш новый класс и устанавливаем несколько удобных переменных. Список allKinds содержит "оригинал" каждого нового типа частиц, которые нам нужны для нашей игры.
Список allParticles это место, где хранятся отображаемые частицы. Этот список будет в конце-концов заполнен сотнями частиц различных видов, и активных, и неактивных. Для каждого кадра мы будем проверять этот список, чтобы увидеть какие частицы нуждаются в обновлении или рендеринге.
Время для действий - определение типа частиц.
//имена конкретного вида частиц
public function defineParticle(
name:String, cloneSource:Stage3dParticle):void
{
trace("New particle type defined: " + name);
allKinds[name] = cloneSource;
Что же произошло?
Предыдущая функция подразумевается быть вызванной во время инициализации вашей игры. Мы посылаем имя также как Stage3dParticle(который содержит геометрию меша, шейдер, текстуру и т.п.) Это частица будет использована как источник для всех будущих частиц этого конкретного вида. Ни одна из этих частиц никогда не рендеритсяЖ они просто ассоциированы со строковым именем, таким как "взрыв" или "всплеск воды" и используются, чтобы предоставить что-то для клонирования.
Время для действий - симулируем все частицы один раз.
Пошаговая функция вызывается каждый кадр во время нашего игрового цикла Этим способом нашей игре нужно только инструктировать всю систему сделать шаг, чтобы убедиться, что каждая из потенциально тысяч частиц обновилась.
//обновляем временные константы пошагового шейдера
public function step(ms:uint):void
{
particlesActive = 0;
for each (particleList in allParticles)
{
for each (particle in particleList)
{
if (particle.active)
{
particlesActive++;
particle.step(ms);
}
}
}
}
Что же произошло?
Когда запускается пошаговая функция системы частиц, нашему игровому движку будет нужно пройти истёкшее время в миллисекундах после предыдущего кадра. В этом случае все анимации частиц независимы от частоты кадров и будут анимироваться с той же частотой независимо от производительности системы.
Она обходит каждый тип частиц (основываясь на имени) в списке allParticles и, затем, каждую частицу того типа, который был создан к тому моменту. Каждая частица, которая в настоящий момент активна, шагает вперёд во времени. Каждая пощаговая функция частицы, которую мы определили в нашем классе Stage3dParticle ранее запущена, так что мы можем обновить движения и прозрачность как необходимо.
Время для действий - рендерим все частицы за раз.
Как и выше, когда система частиц нуждается в обновлении, нам нужно обойти каждую частицу и инструктировать её отрисовать себя.
//визуализируем все активные частицы
public function render(view:Matrix3D,projection:Matrix3D):void
{
totalpolycount = 0;
for each (particleList in allParticles)
{
for each (particle in particleList)
{
if (particle.active)
{
totalpolycount += particle.polycount;
particle.render(view, projection);
}
}
}
}
Что же произошло?
Функция рендеринга системы частиц перебирает все активные частицы и запускает их соответствующие функции рендеринга, так чтобы они отобразились на экране. Мы отслеживаем сколько полигонов визуализируется в каждом кадре (главным образом с целью похвастаться).
Время для действий - создаём частицы по требованию.
Последняя функция, которая нужна нашему классу менеджера системы частиц - это функция, ответственная за то, создавались ли новые частицы или использовались старые, неактивные. Наш игровой движок будет использовать функцию возражения, чтобы "спрашивать" какая новая частица была вызвана.
//либо используем неактивную частицу либо создаём новую
public function spawn(
name:String, pos:Matrix3D, maxage:Number = 1000,
scale1:Number = 1, scale2:Number = 50):void
{
var reused:Boolean = false;
if (allKinds[name])
{
if (allParticles[name])
{
for each (particle in allParticles[name])
{
if (!particle.active)
{
//trace("A " + name + " was reused.");
particle.respawn(pos, maxage, scale1, scale2);
particle.updateValuesFromTransform();
reused = true;
return;
}
}
}
Что же произошло?
В первую очередь мы проверяем, чтобы увидеть если конкретный вид частиц был ранее определён по имени. Если это известный тип, тогда мы перебираем все ранее созданные частицы этого вида и ищем не активированные, которые доступны для повторного использования. Если мы находим её, мы просто вызываем её функцию возрождения, чтобы сбросить некоторые переменные и получить их оживлёнными снова в подходящей новой локации.
Время для действий - создаём новые частицы, если необходимо.
Мы должны учитывать ситуации, где все все доступные частицы на данный момент активны и ни одна не доступна для повторного использования. Продолжим с функции возрождения системы частиц spawn():
else
{
trace("This is the first " + name + " particle.");
allParticles[name] = new Vector.<Stage3dParticle>;
}
if (!reused) // ни одной неактивной частицы не найдено
{
particlesCreated++;
trace("Creating a new " + name);
trace("Total particles: " + particlesCreated);
var newParticle:Stage3dParticle =
allKinds[name].cloneparticle();
newParticle.respawn(pos, maxage, scale1, scale2);
newParticle.updateValuesFromTransform();
allParticles[name].push(newParticle);
}
}
else
{
trace("ERROR: unknown particle type: " + name);
}
}
} // конец класса
} // конец пакета
Что же произошло?
Если ни одной неактивной частицы желаемого типа недоступно, тогда время потратить немного больше памяти и создать новый свежий клон нашей соответствующей "оригинальной" частицы. Мы делаем новую частицу и с этого момента наш список allParticle становится немного больше.
Это всё, что нужно для нашего фантастического нового класса системы частиц. Как вы можете видеть, созданием частиц "по требованию" мы создаём ровно столько, сколько нужно для удовлетворения нужд нашего игрового движка. Эта техника имеет много преимуществ, включая факт, что потребление RAM остаётся относительно константы конкретного числа частиц, которые были однажды созданы.
Всё ещё вполне возможно подвесить ваш компьютер созданием такого большого числа частиц, загрузкой которых система просто не может управлять. В частности, если вы создаёте слишком много частиц в тесной последовательности, вы могли бы попасть в ситуацию, где все доступные частицы в настоящий момент видимы на экране и должны быть созданы новые. Это может помочь, когда вы даёте частицам слишком длинный maxAge так, что они существуют в активном состоянии слишком долго.
Даже с этим новым оптимизированным решением системы частиц, мы должны быть аккуратны и создавать ровно столько частиц, сколько необходимо и не заходить слишком далеко. Если вы генерируете сцену, где используется более чем 100000 полигонов только для частиц, тогда мы должны переосмыслить ваши спецэффекты. В частности помните, что даже если ваша игра запускается плавно на вашем мощном игровом ПК, пользователи будут пытаться запустить её на слабых нетбуках (или планшетах и смартфонах) и не будут иметь то же число лошадиных сил.
Возможно вы можете обойтись более крупными частицами, которые используют меньшее число полигонов, или заставите ваши частицы исчезать быстрее, так чтобы только несколько было на экране в одновременно. Наконец, запомните, что один меш с 500 полигонами рендерится намного быстрее, чем 500 мешей по одному полигону каждый.
Покадровые меши частиц.
Для того, чтобы протестировать нашу новую фантастическую систему частиц, нам нужен круто-выглядящие арт-ресурсы для использования. Нам нужно будет создать несколько новых мешей и прозрачные текстуры. Ключевая техника здесь - это создать два меша для использования как ключевых кадров в нашей системе частиц. Мы ставим себе целью создать для каждого эффект "до" и после".
Отбор текстуры частиц.
Так как цель этой книги - сфокусироваться на программной стороне игрового дизайна, работа по созданию изображений зависит от вас. Здесь есть четыре текстуры, которые мы можем использовать в этом примере. Они были сделаны с использованием Photoshop и сохранены как .PNG файлы с прозрачностью. Так как мы будем рендерить с аддитивным (отбеливающим) режимом наложения, мы можем сохранить их как маленькие (128x128) .JPG изображения с чёрным фоном для того, чтобы сохранить размер файла в вашем SWF минимальным.
Время для действий - лепим отдельную частицу.
Сейчас мы имеем прекрасно выглядящие текстуры, давайте визуализируем их в 3D. Мы не хотим тратить CPU анимируя каждый отдельный спрайт в нашей игре, вместо этого мы хотим "лепить" хорошо выглядящие "капли", включающие множество полигонов. Запустите ваше любимое приложение для 3D моделирования, такое как 3D Studio Max, Blender или Maya и создайте ещё несколько видов спецэффектов, которые вы хотите использовать.
В целях обучения мы пойдём путём создания постоянно используемого примера: взрыва. Начните с создания плоской прямоугольной поверхности и текстурируйте её. Это отдельная частица.
Время для действий - лепим группу частиц.
В типичном взрыве или всплеске воды или группе искр мы обычно ожидаем больше чем одну. Клонируйте эту поверхность несколько раз, пока вы не получите кластер частиц, который выглядит как реальная вещь.
Ещё одна необязательная техника, которую вы можете пожелать использовать (в зависимости от формы и стиля, которого вы надеетесь достичь) - это создать меш, основанный на трёх четырёхугольниках и клонировать его, вместо того, чтобы использовать каждую поверхность индивидуально. Так как четырёхугольник просматриваемый под определённым углом не будет иметь толщины, великолепная техника использовать три четырёхугольника, каждый пересекающийся под определённым углом, так что он виден под каждым углом (x, y, и z). Другими словами, сделаем маленькие "звёзды", которые будут отображаться под любым углом камеры.
Сгруппируйте их вместе и начинайте клонировать эту форму много раз для действительно хорошо смотрящегося облака частиц. Разбросайте их в случайном порядке - возможно более близко расположенными месте в центре кластера и с меньшим разбросом по краям.
Если вы довольны видом вашего взрыва, экспортируйте его как триангулярный .OBJ меш.
Время для действий - лепим второй ключевой кадр.
Теперь начинайте работать на втором кадре вашей анимации частиц. Это "ключевой кадр", который будет использоваться во время интерполированной анимации в вашей игре. Слегка подтолкните вверх каждую частицу, сделайте каждую из них немного больше и поверните чуть-чуть. Есть макросы, которые могут делать это случайным образом для множества объектов одновременно. Сохраните его как второй .OBJ файл, убедившись, что два ваших меша имеют точно одинаковое число полигонов, каждый в том же порядке.
Что же произошло?
В предыдущем шаге мы вкратце обрисовали способ создания двух мешей для использования как состояния "до" и "после" в вашей системе частиц. Класс Stage3dParticle берёт эти два меша и искажает первый во второй с течением времени, используя пользовательский шейдер.
Повторите этот процесс со всеми спецэффектами, которые вы желаете визуализировать в вашей игре. Вы можете иметь много интересного на этом шаге. Используйте ваше воображение и получите другие интересные формы. Например, вы могли бы слепить круглую или кольцеобразную ударную волну. Другая часто используемая форма частицы - это маленький кластер "метеор" - кластер частиц, который указывает в определённом направлении как своего рода направляющее распыление. Этот меш может использоваться для брызг воды, лавы, крови, масла или искр. Конические формы кластеров частиц указывающие в одном направлении, распространяя их наружу, также прекрасно подходят для свечения двигателя, паяльной лампы и вспышек выстрелов из пистолетов.
Так как эта книга о программировании, мы не будем дальше погружаться в арты, но этот шаг - великолепное место, чтобы посходить с ума и отпустить ваше воображение. Stage3D так быстр, что вы совсем не должны волноваться о том, сколько полигонов вы используете. Как было сказано, чем меньше вы используете, тем выше частота кадров, так что пытайтесь создавать меши в интервале до 1000 полигонов. Часто вы можете создать прекрасные кластеры частиц используя меньшее число, чем это.
Объединяем класс системы частиц в нашей игре.
Последний шаг в нашем квесте, чтобы доставить безвозмездное число восхитительных вещей - это получить их и запустить внутри нашей игры. Этот шаг очень прост и потребует только несколько дополнительных строк кода и пару изменений тут и там.
Время для действий - добавляем частицы в вашу игру.
Запустите ваш проект примера из предыдущей главы и откройте ваш файл Stage3dGame.as. Импортируйте ваши новые классы, добавив следующие строки на верх вашего файла рядом со всеми другими важными объявлениями:
import Stage3dParticle;
import GameParticlesystem;
Теперь добавьте ещё несколько переменных внутри определения класса, чуть ниже списков врагов, пуль и бутофории Vector:
//используется в нашем демо системы частиц
private var nextShootTime:uint = 0;
private var shootDelay:uint = 0;
private var explo:Stage3dParticle;
private var particleSystem:GameParticlesystem;
private var scenePolycount:uint = 0;
Теперь встройте новые меши, которые вы создали вместе с новыми текстурами в той же моде, как уже использовалось для моделей нашего космического корабля, астероидов и неба.
//начало взрыва - 336 полигонов
[Embed (source = "explosion1.obj",
mimeType = "application/octet-stream")]
private var explosion1Data:Class;
// конец взрыва - 336 полигонов
[Embed (source = "explosion2.obj",
mimeType = "application/octet-stream")]
private var explosion2Data:Class;
Делайте то же самое для других мешей частиц, которые вы разработали.
Убедитесь, что вы встроили все новые текстуры, которые вы создали, тем же путём, что и другие, используя либо [embed] либо библиотеку Flash IDE. Смотрите исходник предыдущей главы для примеров.
Время для действий - подготавливаем тип частиц для использования.
Теперь, когда ваше новое арт содержимое было встроено, убедитесь, что оно проанализировано - снова тем же способом как существующие модели и текстуры в вашей игре. А именно, добавьте новые текстуры в вашу функцию onContext3DCreate() скопировав и вставив рабочий код используемый другими нашими текстурами.
particle1texture = context3D.createTexture(
particle1bitmap.width, particle1bitmap.height,
Context3DTextureFormat.BGRA, false);
uploadTextureWithMipmaps(
particle1texture, particle1bitmap.bitmapData);
Для того, чтобы определить этот новый тип частиц, добавьте следующие строки в самый низ вашей функции initData():
//создаём систему частиц
particleSystem = new GameParticlesystem();
// определяем тип частиц взрыва
particleSystem.defineParticle("explosion",
new Stage3dParticle(explosion1Data, context3D,
particle1texture, explosion2Data));
Что же произошло?
Предыдущий код, в первую очередь, создаёт новый системный объект GameParticle, который будет, отныне, управлять всеми спецэффектами в вашей игре. Затем мы создаём новый объект Stage3dParticle отправляя данные обоих кадров меша вместе с уже инициализированной текстурой и Context3D. Это версия "оригинальный источник клонов", которая будет дублироваться по требованию нашей системы частиц. Мы дадим этому типу частиц наглядное имя ("взрыв") и инструктируем его, что клонировать каждый раз, когда мы хотим создать новую частицу.
В примере игры из этой главы есть 5 различных видов частиц, определённых тем же способом, как было проиллюстрировано ранее. Для простоты оставим для читателя возможность реализовать различные виды частиц. Используйте ваше воображение!
Время для действий - обновляем функцию renderScene.
Мы должны вставить код в наш цикл рендеринга, чтобы нарисовать все частицы. Как дополнительный бонус, давайте отслеживать число полигонов, которые рендерятся в каждой сцене. Требуется несколько дополнительных строк, чтобы добавить их вместе. Вот как ваша окончательная функция renderScene должна выглядеть:
private function renderScene():void
{
scenePolycount = 0;
viewmatrix.identity();
// смотрим на игрока
viewmatrix.append(chaseCamera.transform);
viewmatrix.invert();
// наклоняем немного
viewmatrix.appendRotation(15, Vector3D.X_AXIS);
// если обзор мышью включён
viewmatrix.appendRotation(gameinput.cameraAngleX,
Vector3D.X_AXIS);
viewmatrix.appendRotation(gameinput.cameraAngleY,
Vector3D.Y_AXIS);
viewmatrix.appendRotation(gameinput.cameraAngleZ,
Vector3D.Z_AXIS);
// отображаем меш игрока из текущим угла камеры
player.render(viewmatrix, projectionmatrix);
scenePolycount += player.polycount;
// обходим все известные сущности и рендерим их
for each (entity in props)
{
entity.render(viewmatrix, projectionmatrix);
scenePolycount += entity.polycount;
}
particleSystem.render(viewmatrix, projectionmatrix);
scenePolycount += particleSystem.totalpolycount;
}
Что же произошло?
Единственным изменением с последнего раза является добавление статистики счётчика полигонов и вызов particleSystem.render. Помните, что функция particleSystemrender() будет обходить каждую частицу и рисовать её для нас. Мы просто посылаем матрицу обзора и матрицу проекции так, чтобы уголы камеры соответствовали и позволяем ей делать свою работу.
Время для действий - добавляем частицы в функцию gameStep.
Теперь, когда мы всё настроили, давайте поэкспериментируем с частицами так, чтобы когда вы зажимаете пробел, всё больше и больше частиц появлялось на экране. Отредактируйте функцию gameStep() так, чтобы новые чатсицы появлялись каждый кадр когда нажат "огонь". Кроме того, мы должны инструктировать систему частиц обновлять каждый кадр, основываясь на прошедшем времени. Здесь новая версия функции:
private function gameStep(frameMs:uint):void
{
// управляем вводом игрока
var moveAmount:Number = moveSpeed * frameMs;
if (gameinput.pressing.up) player.z -= moveAmount;
if (gameinput.pressing.down) player.z += moveAmount;
if (gameinput.pressing.left) player.x -= moveAmount;
if (gameinput.pressing.right) player.x += moveAmount;
if (gameinput.pressing.fire)
{
if (gametimer.gameElapsedTime >= nextShootTime)
{
//trace("Fire!");
nextShootTime =
gametimer.gameElapsedTime + shootDelay;
// случайное положение где-то перед игроком
var groundzero:Matrix3D = new Matrix3D;
groundzero.prependTranslation(
player.x + Math.random() * 200 - 100,
player.y + Math.random() * 100 - 50,
player.z + Math.random() * -1000 - 250);
// создаём новую частицу (или повторно используем неактивную старую)
particleSystem.spawn("explosion", groundzero, 2000);
}
}
// следуем за игроком
chaseCamera.x = player.x;
chaseCamera.y = player.y + 1.5; // над
chaseCamera.z = player.z + 3; // позади
// анимируем атероиды
asteroids1.rotationDegreesX += asteroidRotationSpeed * frameMs;
asteroids2.rotationDegreesX -= asteroidRotationSpeed * frameMs;
asteroids3.rotationDegreesX += asteroidRotationSpeed * frameMs;
asteroids4.rotationDegreesX -= asteroidRotationSpeed * frameMs;
// анимируем свечение двигателя - вращаем быстро и пульсируем медленно
engineGlow.rotationDegreesZ += 10 * frameMs;
engineGlow.scaleXYZ =
Math.cos(gametimer.gameElapsedTime / 66) / 20 + 0.5;
// двигаем все частицы, основываясь на времени
particleSystem.step(frameMs);
}
Что же произошло?
Предыдущего код располагает систему частиц (передавая количество миллисекунд, которые прошли с предыдущего кадра), а также создаёт новый взрыв в случайном местоположении, который устанавливается длительностью 2000 миллисекунд (или 2 секунды), если пробел нажат. Так как эта функция вызывается каждый кадр, если вы удерживаете нажатым пробел, будет создаваться большое число частиц.
Время для действий - отслеживаем статистику частиц.
Так как мы достигли точки, где скриншоты и хвастовство статистикой становятся интересными, поделитесь с друзьями немного большей информацией, может пройти долгий путь, чтобы доказать эффективность алгоритма нашей системы частиц.
В первую очередь, в нашей функции сердцебиения ( которая запускается только каждые несколько секунд и в этот момент используется только в целях отладки), давайте сообщим немного больше статистики. Мы делаем это следующим образом:
//для эффективности эта функция запускается только время от времени
//идеал для вычислений - она не должна вызываться каждый кадр
private function heartbeat():void
{
trace('heartbeat at ' + gametimer.gameElapsedTime + 'ms');
trace('player ' + player.posString());
trace('camera ' + chaseCamera.posString());
trace('particles active: ' + particleSystem.particlesActive);
trace('particles total: ' + particleSystem.particlesCreated);
trace('particles polies: ' + particleSystem.totalpolycount);
}
Наконец, вместе с частотой кадров мы будем сообщать сколько полигонов было визуализировано, так наш скриншот будет иметь что-то, чем можно похвастаться. Отредактируйте одну строку в вашей функции enterFrame, которая обновляет текст FPS и добавьте scenePolycount следующим образом:
fpsTf.text = fps.toFixed(1) +
" fps (" + scenePolycount + " polies)";
Что же произошло?
Мы обновили нашу игру, чтобы отслеживать число полигонов, которые визуализируются каждый кадр. Это поможет во время тестов делать точки отсчёта во времени в отладочном логе ( для определения, если игра становится слишком заполненной врагами или частицами после долгой игровой сессии) и так люди смогут сравнить частоту кадров с числом полигонов на экране в любой отдельный момент.
Так как многие популярные системы частиц на данный момент зависают только с несколькими тысячами частиц (поскольку они имитируют их в CPU и рендерят каждую частицу отдельный вызовом рисования), эта "пакетная имитируемая на GPU" система частиц не оставит равнодушными ваших друзей с её невероятной производительностью. Вы можете ожидать быть способными анимировать 50,000+ частиц за кадр и при этом поддерживать гладкие 60fps на достойных игровых платформах.
Это всё! Наша игра может теперь выкачивать огромное число частиц различных видов. Предыдущий пример кода только добавляет тип частиц "взрыв", но в вашей собственной игре вы можете сделать также для нескольких типов частиц.
Давайте посмотрим новую систему частиц в действии!
Вы можете посмотреть законченное демо Главы 8 здесь:
http://www.mcfunkypants.com/molehill/chapter_8_demo/
Кроме того, полный исходный код доступен по следующему URL:
http://www.packtpub.com/adobe-flash11-stage3d-molehill-gameprogramming-beginners-guide/book
Теперь, когда мы реализовали новый фантастический класс сущностей частиц и AGAL код шейдера будет делать всю анимацию частиц в GPU для скорости и системы частиц, которая поддерживает бассейн повторно используемых частиц, наша игра способна на потрясающую привлекательность. Так как мы группируем геометрию и рендерим сотни и даже тысячи частиц за один вызов drawTriangles, наша игра может легко ренедрить десятки тысяч частиц на экране без малейших затруднений.
Рендеринг частиц в GPU с помощью пакетов, повторного использования, гипер-оптимизированная техника вырисовывающиеся в этой главе освобождают CPU для других вещей. Это значит ваша игра имеет множество лошадиных сил позволяя ренедрить другие виды мешей на вашем уровне, так же как CPU доступен для обработки вводов, событийных звуков, выполнения искусственного интеллекта и проверки столкновений и другого без необходимости делать тяжёлую работу, требующуюся для эффектов.
Викторина - зарабатываем очки опыта.
Здесь будут несколько быстрых вопросов, чтобы протестировать ваше понимание. Если вы можете ответить на них, вы официально готовы перейти на следующую ступень в вашем грандиозном приключении:
1. Почему мы просто не рендерим тысячи индивидуальных частиц отдельно вместо больших пакетов?
а) Потому что Stage3D может отображать только низкополигональные сцены.
б) Потому что каждый вызов рисования даёт дополнительные расходы и слишком много их приведут к низкой частоте кадров.
в) Потому что нам пришлось бы писать уникальный шейдер для каждой частицы.
г) Потому что это было бы слишком скучно для нашего компьютера.
Почему мы создали два меша для нашего взрыва вместо только одного?
а) В качестве средства для предварительного расчёта траектории многочисленных полигонов
б) Чтобы нам не нужно было использовать CPU чтобы делать имитацию работы
в) Потому что мы используем второй для плавной анимации частиц в AGAL шейдере
г) Всё вышеперечисленное.
Какую технику мы используем, чтобы увеличить частоту кадров и избежать утечек памяти и сборщика мусора временных переменных во время цикла рендеринга?
а) Мы сделали все наши переменные приватными
б) Мы допускаем, что все юзеры имеют бесконечную RAM
в) Мы сделали "бассейн" повторно используемых частиц, которые могут воссоздаваться снова и снова.
г) Мы скомпилировали наш исходный код очень осторожно.
Вперёд, герой - изобретаем крутые эффекты частиц.
Ваш сторонний квест на этот раз это спроектировать группу круто-выглядящих эффектов частиц для вашей игры. Вместо простого расширяющегося взрыва, почему бы не добавит вращающийся торнадо (состоит из множества пылинок или спрайтов туманного облака)? С другой стороны, создайте текстуру, состоящую из маленьких голубых капелек и спроектируйте "брызги" подходящие для передней части парусного судна на берегу океана. Возможно ваша игра будет шутером и вы захотите много запёкшейся крови. Если так, сделайте брызги крови, которые появляются из пулевых ранений вместе со светящимися вспышками выстрелов выходящих из вашего оружия.
Частицы могут быть также использованы в других, менее очевидных случаях. Например, в детективной игре, слабые следы на песке могут появляться через каждые несколько футов движения игроков. Обломки, такие как мусор или бумага в гетто, уносимые ветром, могут добавить атмосферу в игру. В подводной игре маленькие точки органического материала, плавающие в воде, могут придать вашему движению больше глубины. В симуляторе знакомств или визуальном романе, возможно, персонаж так очарован другим, что маленькие красные сердечки плавают перед их глазами, так как они безумно влюблены.
Итого.
В этой главе нам удалось достичь следующих рубежей:
Мы написали AGAL шейдер, который плавно анимирует и уменьшает частицы с течением времени
Мы разработали основной класс частиц с двухкадровыми данными из двух мешей.
Мы создали систему частиц, которая управляет и повторно использует множество типов частиц.
Мы обновили нашу игру, чтобы показать эти новые спецэффекты.
Уровень 8 достигнут!
Поздравляем! Вы почти в конце вашего эпического путешествия. Хотя наша игра ещё очень проста, мы уже достигли большинства рубежей напути к созданию высокополигональной, удивительно красивой 3D игры следующего поколения во Flash. На горизонте все окончательные изменения, которые делают видеоигру. Эти последние куски в паззле представляют захватывающее заключение нашего грандиозного приключения.
Остался один последний квест до завершения. Связывание свободных концов: полировка. Это вызов, который ожидает нас в следующих двух главах. Сокровище, которое мы получаем в награду, после всей нашей тяжёлой работы это то, что скоро мы сможем поиграть в законченную 3D игру нашего собственного производства.
Переведено на Нотабеноиде
http://notabenoid.org/book/40281/205229
Переводчики: Orion45
Настройки
Готово:
