Сожалеем, но текст оригинала доступен только зарегистрированным пользователям. |
Вы достигли уровня 8. Следующий шаг в нашем квесте графического великолепия это реализовать кое-что привлекательное. Система частиц и спецэффекты необходимые для добавления в вашу игру визуальные составляющие.
Orion45
16.03.14 в 08:15
|
|
Вещи становятся действительно захватывающими сейчас. Вы возможно можете уже увидеть свет в конце туннеля: наша игра принимает форму и мы уже изучили так много. Сейчас мы уже достигли некоторого основнго геймплея - движение, анимация, GUI heads-up дисплея, таймеры и классы ввода - следующий шаг это сделать это хорошо выглядящим.
Orion45
16.03.14 в 13:43
|
|
В этой главе мы реализуем систему, способную производить огромное количество спецэффектов. Есть серьёзные основания принять во внимание, помимо всего прочего, что мы не хотим, чтобы наши спецэффекты разрушили нашу гладкую шелковистую частоту кадров.
Orion45
19.03.14 в 18:39
|
|
Наш текущий квест. Принимая во внимание оптимизацию и абстрактную способность к повторному использованию, давайте создадим менеджер системы частиц, способный к рендерингу безумного числа частиц и, в то же время, имеющий минимальный эффект на частоту кадров. Мы хотим быть способны выводить всё от взрывов, дыма, искр, ударных волн, брызг крови, бликов, брызг воды или лавы и до пылинок парящих в воздухе.
Orion45
19.03.14 в 18:48
|
|
Что мы хотим это быть способными отображать МАССУ частиц с прекрасной частотой кадров: Прежде чем мы погрузимся, мы должны принять к сведению некоторые цели проектирования для нашей фантастической системы частиц. Проектирование для производительности.
Orion45
19.03.14 в 18:53
|
|
Многие авторы движков Flash 3D игрв последнее время имеют проблемы с хорошей производительностью системы частиц. Причина в том, что наш первый инстинкт это просто рисовать тысячи изображений спрайтов по отдельности. Конечно,это приводит к тому, что есть избыток в вычислениях трансформаций матриц и перерисовке состояния, которое вызывает каждый рисунок, и единственный способ как мы можем достичь большого числа частиц во Flash это группировать их большими кусками. Ещё лучше, если мы сможем избежать выполнения анимационных симуляций на CPU и возложим все вычисления на GPU, используя AGAL. Наши движки частиц могут приносить пользу фактически без использования CPU.
Orion45
19.03.14 в 19:09
|
|
Мы хотим систему частиц, которая позволяет мегасложные системы частиц почти бесконечной пыльцы. Возможное решение это создать специализированный вершинный буфер для всей системы частиц.
Orion45
20.03.14 в 14:59
|
|
С этой техникой вся система частиц будет управляться на GPU фактически без избыточного рендеринга для анимации гигантской системы частиц. Почему? Целая вещь рендерится за один вызов отрисовки. По-существу мы "предрассчитываем" целую анимацию для ваших взрывов и других эффектов. Не волнуйтесь о каждом отдельном спрайте во взрыве, вы просто рисуете целую группу в одно время и поручаете GPU делать всю тяжёлую работу.
Orion45
20.03.14 в 16:04
|
|
Используя эту технику мы сможем реализовать полностью анимированную в GPU систему частиц (60fps с 40000 частиц, где каждая движется с различным направлением, поворотом и увеличивается во время затухания), которая использует AGAL для всех симуляций - не CPU. Проектирование для повторного использования
Orion45
21.03.14 в 15:05
|
|
Мы не хотим жёстко кодировать рендеринг наших спецэффектов для конкретного вида эффектов. Поэтому мы нацелимся на способности отправлять системе любой вид мешей: от простых плоских спрайтов, состоящих из базовых четырёхугольников, до целых групп полигонов, которые составляют сложный взрыв.
Orion45
21.03.14 в 15:10
|
|
Другая форма повторного использования это способность использовать ранее созданные частицы снова и снова. Мы также не хотим увязнуть в CPU и поглощать массу RAM каждый кадр, создавая и разрушая тысячи мешей снова и снова. Делая так, мы убьём частоту кадров, потому что время, требующееся для загрузки данных меша (или даже создания простых AS3 объектов) добавляется - если мы собираемся порождать частицы снова и снова во время игры, нет причин непрерывно инициализировать их и затем разрушать, когда они пропадают.
Orion45
22.03.14 в 16:51
|
|
Поэтому помимо выполнения всех симуляций отдельно на GPU, другой техникой оптимизации, используемой здесь, является создание "бассейна частиц", так что эти частицы повторно используются, если не активны. Это позволяет избежать проблем со сборщиком мусора, которые приводят к сбою частоты кадров: мы не "порождаем" новые частицы каждый кадр, когда возникает новый взрыв, мы просто повторно используем старые и только создаём новые, если нет доступных неактивных. Результат этого - частота кадров остаётся в районе 60 fps, создаём ли мы сорок тысяч частиц на экране или вовсе ни одной. Анимация с использованием AGAL.
Orion45
23.03.14 в 12:14
|
|
AGAL и место установки для техники, описанной ранее относительно упрощено. Мы создаём два вершинных буфера. Один содерджит начальные позиции и другие конечные позиции предвычисляемые для каждой вершины. Например, вместо создания тысяч GameEntities, создаём один меш из 10000 полигонов со следующими данными вершинного буфера: x1,y1,z1,x2,y2,z2. Вместо того, чтобы передвигать каждую частицу через математические матрицы AS3, движение спрайта предвычисляется и само сохраняется в вершинном буфере.
Orion45
24.03.14 в 18:23
|
|
Однажды создав эту частицу "группы геометрии" мы можем отправить дельта значение как констатный регистр вершинного буфера, который варьируется от 0 до 1 каждый раз. В каждом кадре, когда мы рендерим меш, у нас будет ваша вершинная программа AGAL , которая интерполирует позицию x, y, z каждой вершины от начальной до конечной позиции. Каждая вершина будет гладко двигаться от первой ко второй позиции.
Orion45
25.03.14 в 16:51
|
|
Предрассчитывая движения каждой частицы один раз и затем просто получая AGAL, чтобы плавно переместить из одного места в другое, мы можем симулировать гравитацию, ветер, увеличение размера таких вещей как рассеивание дыма в воздухе и другие.
Orion45
26.03.14 в 12:43
|
|
Для того, чтобы достичь великолепной производительности во время геймплея, мы не хотим делать никаких сложных вычислений в AS3. Вместо этого, однажды заполнив два вершинных буфера, описанных ранее, всё, что мы должны делать каждый кадр это инструктировать Stage3D сколько времени прошлою Ранее, когда мы рендерили наш меш частиц, мы отправляли это значение в Stage3D для использования в шейдере.
Orion45
26.03.14 в 12:48
|
|
Ключевое дизайнерское решение здесь - НЕ рендерить или симулировать каждую частицу отдельно. В зависимости от вычислительных возможностей вашего компьютера (и, в частности, скорости заполнения вашей видеокарты) вполне осуществимо, что ваша игра сможет похвастаться сценой, содержащей 100000+ частиц и, в то же время, всё ещё выполняющейся при 60fps - с возможностью сохранить все ваши другие модели, т.к. мы не используем CPU для всякой тяжёлой работы.
Orion45
26.03.14 в 12:56
|
|
Для того, чтобы успешно завершить этот квест, мы должны выполнить следующее: Создать основной класс сущностей частиц Запрограммировать AGAL для ключевых анимаций вершин Закодить менеджер системы частиц, чтобы контролировать множество частиц Спроектировать некоторые хорошо выглядящие арты Объединить систему частиц с нашей игрой Время обновить приятности нашей игры. Эта эффективная технология "пакетной системы частиц в GPU" даст вам шик, необходимый для некоторых превосходных скриншотов и внутриигровой последовательности действий. Давайте приступим!
Orion45
26.03.14 в 14:36
|
|
Основной класс системы частиц. Первый шаг в добавлении красивостей к нашей игре это дать определение что же такое частицы. Постепенно наша игра породит сотни для использования во всех видах спецэффектах, так что давайте сделаем это абстрактным и сможем использовать многочисленные виды частиц - от брызг воды до искр, которые летят от скрещивающихся мечей.
Orion45
26.03.14 в 14:41
|
|
Одна частица в нашей системе спецэффектов может быть отдельным полигоном или предварительно подготовленным "пакетом" множества полигонов. Так как мы хотим иметь множество многоугольников с минимальным расходом частоты кадров, мы позаботимся о всех эффектах (таких как взрыв) как единственной сущности, даже хотя меш, который это использует будет состоять из сотен тысяч полигонов.
Orion45
26.03.14 в 14:49
|
|
Мы хотим, чтобы наш класс сущностей частиц поддерживал данным меша так же как текстура или шейдер. Это звучит подозрительно похоже на класс Stage3dEntity, который мы уже запрограммировали. Хорошие новсти это то, что мы только должны добавить несколько незначительных дополнений в этот класс сущностей, так что вместо создания новых впечатляющих видов AS3 объектов с нуля, мы просто расширим основной класс сущностей и добавим несколько дополнительных функций и переменных.
Orion45
26.03.14 в 14:57
|
|
Время для действий - расширяем класс сущностей для частиц. Создадим новый файл в вашей папке исходного кода и назовём его 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 {
Orion45
26.03.14 в 15:02
|
|
Что же произошло? Последняя строка предыдущего кода инструктирует Flash "расширить" или взять все функции которые были реализованы в нашем классе Stage3dEntity и предполагает, что они такие же в новом классе. В этом случае наш класс частиц незамедлительно имеет все вспомогательные функции во всём их ассортименте, которые мы так тяжело разработали ранее.
Orion45
26.03.14 в 16:20
|
|
Это использование наследования классов помогает сэкономить много времени, позволяя повторно использовать код. Это сохраняет базовую функциональность в одном месте. Если мы обновляем этот базовый класс или исправляем ошибку, тогда все классы, которые расширяют его, также автоматически обновляются.
Orion45
26.03.14 в 16:25
|
|
Время для действий - добавляем свойства частиц. Теперь мы хотим дать нашему классу несколько расширенных свойств, относящихся к частицам. Мы сделаем это следующим образом: 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;
Orion45
26.03.14 в 16:28
|
|
Что же произошло? Эти свойства будут использованы, чтобы симулировать нашу систему частиц. Отслеживая возраст, мы можем вычислять подходящий размер и цвет с течением времени, так что взрыв, например, начинается маленьким и увеличивается в размере постепенно затухая. Время хранится в миллисекундах, так что по умолчанию будет создана частица, которая существует ровно одну секунду. Время для действий - программируем конструктор класса частиц.
Orion45
28.03.14 в 13:30
|
|
Теперь, когда мы определили переменные, которые нам нужны, давайте напишем новую функцию конструктора класса. Мы не хотим использовать ту, которая перешла к нам от нашего оригинального класса Stage3dEntity, так что мы используем ключевое слово "override", чтобы заменить её нашей новой версией. Хотя это похоже на основную сущность, наши частицы могут иметь необязательный второй меш, который используется в покадровой анимации позиций вершин. Мы также не должны передавать ничего шейдеру, так как мы определим особый AGAL шейдер позже.
Orion45
28.03.14 в 13:43
|
|
//Класс конструктора - второй меш определяет //конечную позицию каждой вершины в первом public function Stage3dParticle( mydata:Class = null, mycontext:Context3D = null, mytexture:Texture = null,
Orion45
28.03.14 в 13:45
|
|
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; }
Orion45
28.03.14 в 13:48
|
|
Что же произошло? Этот конструктор просто устанавливает сущность частиц, чтобы она рендерилась с правильным режимом наложения. Если мы отправляем второй меш в параметры, тогда меш также парсится и готов к использованию в нашем шейдере. В зависимости от того, использует ли частица отдельный меш или интерполирует форму из неё после этого, создаётся подходящий шейдер.
Orion45
28.03.14 в 16:16
|
|
Время для действий - клонируем частицы. Как только нам нужно скомпилировать 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; }
Orion45
28.03.14 в 16:21
|
|
Что же произошло? Всё что делает эта функция - это копирует переменные, необходимые для новой копии текущей частицы. Это одна из различных оптимизаций, которая будет позволять нашей игре рендерить МАССУ частиц, используя минимальные системные ресурсы.
Orion45
28.03.14 в 16:24
|
|
Время для действий - генерируем числа, используемые для анимации. Следующие две функции просто удобные алгоритмы, которые вы можете захотеть использовать для симуляции частиц. Они плавно интерполирую значение используя функцию синуса, так, что оно "колеблется" и завершается. Это функция очень удобна как ваши проектные частицы, которые должны качаться вверх и вниз или блекнуть и постепенно плавно исчезать. private var twoPi:Number = 2*Math.PI;
Orion45
28.03.14 в 17:47
|
|
//возвращает число с плавающей запятой из -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);
Orion45
28.03.14 в 17:49
|
|
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; }
Orion45
28.03.14 в 17:49
|
|
Что же произошло? Что же эти функций "колебаний" делают? Они используются для того, чтобы генерировать числа, которые плавно меняются от нуля до единицы (и возможно возвращаются обратно снова), основываясь на том, как много времени прошло. Они отправляются в AGAL код в нашем шейдере частиц, чтобы наши частицы появлялись и исчезали и заставляя их увеличиваться в размерах.
Orion45
30.03.14 в 07:52
|
|
Время для действий - симулируем частицы. Следующий шаг - это запрограммировать пошаговую функцию. Это функция будет в итоге вызываться один раз каждый отдельный кадр, для каждой отдельной частицы вашим классом менеджера системы частиц. Поэтому для этой функции важно быть настолько простой, насколько это возможно. 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;
Orion45
30.03.14 в 08:02
|
|
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]; }
Orion45
30.03.14 в 08:03
|
|
Что же произошло? Используя время, прошедшее с предыдущего кадра, эта функция использует функции колебаний, чтобы сгенерировать список чисел, которые постепенно идут от нуля до единицы или от единицы до нуля, в зависимости от того, что требуется. Во-первых, проверяется, чтобы увидеть, если возраст частицы вышел за пределы её желаемого времени жизни. Если так, частица более не нуждается в обновлении или рендеринге. Если она наоборот ещё активна, мы вычисляем возраст, масштаб и rgba значения, которые будут использованы нашим шейдерам, чтобы выполнить анимацию.
Orion45
30.03.14 в 10:42
|
|
Время для действий - возрождаем частицы. Как упоминалось ранее, мы хотим создавать совершенно новые частицы только когда нет ни одной неактивных частиц, которые могут быть использованы повторно. Это то, как мы способны получить так много полигонов на экране с такой великолепной производительностью. Избегая создания и разрушения новых объектов каждый кадр, мы сохраняем требования памяти постоянными во время игрового цикла.
Orion45
30.03.14 в 15:35
|
|
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;
Orion45
30.03.14 в 15:36
|
|
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()); }
Orion45
30.03.14 в 15:37
|
|
Что же произошло? Функции возрождения используются нашим предстоящим менеджером системы частиц чтобы сбросить требуемые переменные так, чтобы существующие частицы были готовы к "новой жизни". Всякий раз, когда наши игры должны создавать, например, новый взрыв, мы просто должны инструктировать существующую частицу начинать анимацию с нового местоположения. Мы можем также корректировать размер или продолжительность жизни нашей частицы, если необходимо, так что мы можем делать такие вещи как создавать взрывы больше и меньше в зависимости от причиняемого урона.
Orion45
30.03.14 в 16:27
|
|
Одно важное замечание: мы поворачиваем наш меш взрыва "по направлению вниз" в этом примере потому что данные примера в нашем втором .OBJ файле имеют каждую частицу в слегка высоком положении. В зависимости от вашего исходного арта, вы можете не захотеть включить эту строчку. Вы могли бы также захотеть усилить этот алгоритм, чтобы он был способен отключить, например, случайную ориентацию, которая используется здесь, чтобы заставить каждый взрыв выглядеть более отличным от предыдущего.
Orion45
30.03.14 в 16:34
|
|
Переменные возраста, масштаба и rgba специализируются со значениями, которые имеют смысл для каждого первого кадра нашего шейдера частиц. Мы будем изменять эти значения каждый кадр для того, чтобы меш исчезал или выростал, отправляя их в наш AGAL шейдер для использования как вершинные и фрагментные константы.
Orion45
30.03.14 в 16:39
|
|
Время для действий - рендерим частицы. Точно так же как мы расширили функцию конструктора по умолчанию класса Stage3dEntity, мы хотим также написать нашу собственную функцию рендеринга частиц. Перезапишем функцию рендеринга следующим образом: //оптимизация: повторно используем те же самые временные матрицы override public function render( view:Matrix3D, projection:Matrix3D, statechanged:Boolean = true):void { // только рендеринг, если это установлено if (!active) return;
Orion45
30.03.14 в 16:43
|
|
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);
Orion45
30.03.14 в 16:45
|
|
Мы начинаем с первого обновления масштаба нашего меша, чтобы увеличивать или сокращать нашу частицу в зависимости от значений, которые мы отправили в функцию возрождения. Простое масштабирование обновляет нашу матрицу трансформации используя переменную ageScale. Затем она смешивается с матрицей обзора и проекции так же, как мы делали во всех предыдущих примерах визуализации. //Устанавливаем вершинный программный регистр vc4 //в наше время и масштаб из (0..1) //используется, чтобы интерполировать позицию вершины с течением времени context.setProgramConstantsFromVector( Context3DProgramType.VERTEX, 4, ageScale); // Устанавливаем фрагментный програмный регистр fc0 // в наше время и масштаб из (0..1) // используется, чтоб интерполировать прозрачность с течением времени context.setProgramConstantsFromVector( Context3DProgramType.FRAGMENT, 0, rgbaScale);
Orion45
30.03.14 в 16:55
|
|
Затем мы отправляем наши интерполированные значения в шейдер. Заполняя вершинные программные константы с ageScale и фрагментную программную константу с rgbaScale, наш Stage3D шейдер сможет сделать отсаток работы за нас. Помните, что те две переменные содержат плавно интерполированный список чисел (четыре в каждом), которые идут от 1 до 0 или от 0 до 1 с течением времени, как это необходимо. Функция step() это функция, где эти значения обновляются в каждом кадре.
Orion45
30.03.14 в 17:02
|
|
//Устанавливаем программу 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); } // конец функции рендеринга
Orion45
30.03.14 в 17:06
|
|