Статьи

Создание 3D-эффектов с помощью движка частиц Stardust

В моем предыдущем уроке Shoot Out Stars с двигателем частиц Stardust я объяснил основной рабочий процесс Stardust. На этот раз мы пойдем дальше и рассмотрим несколько методов для создания настоящих эффектов трехмерных частиц!


Мы начнем с демонстрации того, как использовать собственный 3D-движок Stardust . Затем я покажу вам, как заставить Stardust работать с Papervision3D; мы создадим эффекты трехмерных частиц с помощью класса частиц Papervision3D и класса DisplayObject3D.


Мы собираемся забрать, где мы остановились в первый урок. В прошлый раз мы создали частицы звезд и кругов, которые вылетали из точки, увеличиваясь до максимального размера, а затем сжимаясь до нуля, постепенно перемещаясь со временем (так называемый эффект затухания). На этот раз мы сделаем то же самое, но в 3D. Вместо частиц, движущихся по кругу, они будут двигаться в сфере.


Как и прежде , сначала создайте новый документ Flash с размерами 640×400, частотой кадров 60 кадров в секунду и темным фоном (я использовал синий градиент).


Нарисуйте звезду и белый круг, затем конвертируйте их в символы отдельно. Это два символа, которые мы собираемся использовать в качестве частиц позже. Назовите символ звезды «Звезда» и символ круга «Круг», экспортированный для ActionScript с теми же именами классов.

(Если вы не большой артист, вы можете загрузить исходный код вверху страницы и использовать мои символы из библиотеки моего FLA.)


Нажмите « Окно»> «Компоненты», чтобы открыть панель «Компоненты», затем перетащите кнопку из папки интерфейса пользователя на сцену. Установите метку «Пауза» и назовите ее «pause_btn». Мы собираемся использовать эту кнопку, чтобы приостановить эффекты трехмерных частиц, что позволит пользователям вращать камеру, чтобы лучше почувствовать трехмерную среду.


Создайте новый класс документа и назовите его StarParticles3D.

1
package { import flash.display.Sprite;

Не знаете, как использовать класс документа? Прочтите этот краткий совет.


Три основных пакета в Stardust:

  • idv.cjcat.stardust.common : содержит общие элементы для эффектов 2D и 3D частиц.
  • idv.cjcat.stardust.twoD : содержит специальные элементы для эффектов 2D-частиц.
  • idv.cjcat.stardust.threeD : содержит специальные элементы для эффектов трехмерных частиц.

В предыдущем уроке мы использовали инициализаторы и действия из общего и двумерного пакетов. В этом уроке мы все еще будем использовать элементы из общего пакета, но не из двумерного пакета. Вместо этого мы будем использовать элементы из трехмерного пакета.

Структура классов в трехмерном пакете почти такая же, как и в двумерном, за исключением того, что элементы имеют дополнительное измерение. Трехмерный элемент имеет то же имя, что и его двумерный аналог, но его имя оканчивается на «3D». Например, действие Move3D в 3D-пакете обновляет положения частиц в 3D-пространстве в соответствии со скоростями, так же как его 2D-аналог в 2D-пакете, действие Move .


Создайте новый файл AS с именем StarEmitter.as ; внутри него создайте новый класс StarEmitter , который расширяет класс Emitter3D:

01
02
03
04
05
06
07
08
09
10
11
package {
    import idv.cjcat.stardust.threeD.emitters.Emitter3D;
     
    public class StarEmitter extends Emitter3D {
         
        public function StarEmitter(clock:Clock) {
            //pass the clock object to the superclass’s constructor
            super(clock);
        }
    }
}

Помните параметр Clock? Он используется для контроля скорости создания частиц. Нам нужно включить его в функцию конструктора, чтобы мы могли передать ему Clock позже.

Поскольку мы разрешаем пользователям приостанавливать эффекты частиц, мы собираемся упаковать все действия в один объект CompositeAction, который, по сути, представляет собой группу действий. Деактивировав это единственное составное действие, мы можем «отключить» все базовые действия. Объявите переменную для составного действия в классе эмиттера. Мы получим доступ к этой переменной в классе документа, поэтому она должна быть публичной:

1
public var pausibleActions:CompositeAction;

Объявите константы, которые будут использоваться в качестве параметров частиц в классе эмиттера. Мы уже рассмотрели назначение этих констант в предыдущем уроке. Большинство имен говорят сами за себя. Они идут внутри класса, но за пределами функции конструктора. Не стесняйтесь возвращаться сюда позже и изменить цифры, чтобы увидеть эффект.

01
02
03
04
05
06
07
08
09
10
11
private static const LIFE_AVG:Number = 30;
private static const LIFE_VAR:Number = 10;
private static const SCALE_AVG:Number = 1;
private static const SCALE_VAR:Number = 0.4;
private static const GROWING_TIME:Number = 5;
private static const SHRINKING_TIME:Number = 10;
private static const SPEED_AVG:Number = 30;
private static const SPEED_VAR:Number = 10;
private static const OMEGA_AVG:Number = 0;
private static const OMEGA_VAR:Number = 5;
private static const DAMPING:Number = 0.1;

В предыдущем уроке я продемонстрировал, как использовать SwitchInitializer для создания частиц с различными экранными объектами. Я использовал инициализатор DisplayObjectClass, который инициализирует внешний вид частиц с помощью экранных объектов. Это было для эффектов 2D частиц; здесь мы собираемся использовать его трехмерный аналог, инициализатор DisplayObject3D.

Добавьте следующий код в функцию конструктора эмиттера:

1
2
3
4
5
//switch initializers for star and circle particles
var doc1:DisplayObjectClass3D = new DisplayObjectClass3D(Star);
var doc2:DisplayObjectClass3D = new DisplayObjectClass3D(Circle);
var si:SwitchInitializer = new SwitchInitializer([doc1, doc2], [1, 1]);
addInitializer(si);

То же, что и в предыдущем уроке; добавьте другие инициализаторы, показанные ниже. Обратите внимание, что некоторые из них имеют имена, похожие на названия в предыдущем уроке, но заканчиваются на «3D».

  • Инициализатор Position3D инициализирует положения частиц. Как и его 2D-аналог, инициализатор положения, он принимает один параметр конструктора, объект зоны. Однако на этот раз он не принимает объект Zone, который представляет 2D-зону; вместо этого он принимает объект Zone3D, представляющий зону в трехмерном пространстве. Класс SinglePoint3D расширяет класс Zone3D и является 3D-версией класса SinglePoint.
  • То же самое относится и к классу Velocity3D. Класс SphereShell по сути является 3D-версией класса SectorZone. Здесь мы устанавливаем центр оболочки сферы равным (0, 0, 0), средний радиус — SPEED_AVG, а изменение радиуса — SPEED_VAR.
  • Инициализаторы Rotation3D и Omega3D работают точно так же, как и их 2D-аналоги, Rotation и Omega. Однако есть одно небольшое различие в параметрах конструктора: они принимают три объекта Random вместо одного. Это связано с тем, что теперь в трехмерном пространстве есть три оси вращения, поэтому для них требуются три случайных значения вращения. В этом примере мы создаем «двумерные рекламные щиты» в трехмерном пространстве (т. Е. Плоские объекты), поэтому очевидно только вращение оси Z; Вот почему первые два параметра, Случайные объекты для осей X и Y, имеют нулевые значения.

Этот код входит в функцию конструктора StarEmitter:

1
2
3
4
5
6
addInitializer(new Life(new UniformRandom(LIFE_AVG, LIFE_VAR)));
addInitializer(new Scale(new UniformRandom(SCALE_AVG, SCALE_VAR)));
addInitializer(new Position3D(new SinglePoint3D()));
addInitializer(new Velocity3D(new SphereShell(0, 0, 0, SPEED_AVG, SPEED_VAR)));
addInitializer(new Rotation3D(null, null, new UniformRandom(0, 180)));
addInitializer(new Omega3D(null, null, new UniformRandom(OMEGA_AVG, OMEGA_VAR)));

Создайте составное действие и добавьте к нему некоторые действия. Затем добавьте это составное действие к эмиттеру; это заставит частицы выполнять действия. Вы видели эти действия в предыдущем уроке (некоторые из них в 2D-версии), поэтому я не буду объяснять их заново. Опять же, этот код входит в функцию конструктора StarEmitter:

1
2
3
4
5
6
7
8
pausibleActions = new CompositeAction();
pausibleActions.addAction(new Age());
pausibleActions.addAction(new DeathLife());
pausibleActions.addAction(new Move3D());
pausibleActions.addAction(new Spin3D());
pausibleActions.addAction(new Damping3D(DAMPING));
pausibleActions.addAction(new ScaleCurve(GROWING_TIME, SHRINKING_TIME));
addAction(pausibleActions);

Хорошо, мы закончили с эмиттером. Теперь пришло время построить наш класс документов.

Сначала объявите константы для радиуса орбитальной камеры, расстояния камеры от начала координат и скорости излучателя:

1
2
private static const CAMERA_RADIUS:Number = 250;
private static const PARTICLE_RATE:Number = 0.5;

(Как и прежде, константы идут внутри класса, но вне функции конструктора.)

Затем объявите переменные для эмиттера, постоянных часов и DisplayObjectRenderer3D (в том же месте, что и константы):

1
2
3
private var emitter:StarEmitter;
private var clock:SteadyClock;
private var renderer:DisplayObjectRenderer3D;

В конструкторе инициализируйте часы, эмиттер и рендер. Также установите исходное положение и направление камеры, чтобы она смотрела на начало координат:

1
//create the clock and emitter clock = new SteadyClock(PARTICLE_RATE);

Создайте в классе документа функцию-обработчик для обработки события нажатия кнопки паузы:

01
02
03
04
05
06
07
08
09
10
11
private function togglePause(e:MouseEvent):void {
    if (e.target.label == «Pause») {
        e.target.label = «Resume»;
        clock.ticksPerCall = 0;
        emitter.pausibleActions.active = false;
    } else {
        e.target.label = «Pause»;
        clock.ticksPerCall = PARTICLE_RATE;
        emitter.pausibleActions.active = true;
    }
}

… затем зарегистрируйте слушатель для кнопки паузы в функции конструктора:

1
pause_btn.addEventListener(MouseEvent.CLICK, togglePause);

Создайте обработчик для события ENTER_FRAME. Это наш основной цикл. Он обновляет положение камеры, вызывая метод updateCamera () (который мы закодируем через минуту) и вызывает метод step () эмиттера, который поддерживает действие частиц:

1
2
3
4
private function mainLoop(e:Event):void {
    updateCamera();
    emitter.step();
}

Снова зарегистрируйте слушателя в конструкторе:

1
addEventListener(Event.ENTER_FRAME, mainLoop);

Теперь определите метод updateCamera (), вызванный на предыдущем шаге. Это используется для перемещения камеры в трехмерном пространстве в зависимости от положения мыши. (Если вам нужна дополнительная информация о том, как это работает, ознакомьтесь с этой статьей в Википедии .)

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

01
02
03
04
05
06
07
08
09
10
11
private function updateCamera():void {
    var theta:Number = 0.02 * (mouseX — 320);
    var phi:Number = 0.02 * (mouseY — 200);
    phi = StardustMath.clamp(phi, -StardustMath.HALF_PI, StardustMath.HALF_PI);
     
    var x:Number = CAMERA_RADIUS * Math.cos(theta) * Math.cos(phi);
    var y:Number = CAMERA_RADIUS * Math.sin(phi);
    var z:Number = CAMERA_RADIUS * Math.sin(theta) * Math.cos(phi);
    renderer.camera.position.set(x, y, z);
    renderer.camera.direction.set(-x, -y, -z);
}

Обратите внимание, что я использовал метод StardustMath.clamp (); это гарантирует, что значение phi находится между положительной и отрицательной половиной PI.


Хорошо, мы закончили! Это все, что нам нужно сделать, чтобы 3D-излучатель работал с собственным 3D-движком Stardust. Давайте посмотрим на результат. Вы можете нажать кнопку паузы, чтобы приостановить эффект частиц, и переместить мышь вокруг, чтобы вращать камеру:

Если вы хотите увидеть полный исходный код, посмотрите в папке «01 — Stardust Native 3D Engine» в Source.


Переключиться с родного 3D движка Stardust на Papervision3D легко. Нам просто нужно использовать другой рендерер и инициализатор экранного объекта.

(Никогда раньше не использовал Papervision3D? Взгляните на этот урок для начинающих .)

Сначала мы будем использовать класс частиц Papervision3D. Возможно, вы не знакомы с этим; Я покажу вам, как использовать более распространенный класс DisplayObject3D позже.


Измените следующий код в классе эмиттера:

1
2
var doc1:DisplayObjectClass3D = new DisplayObjectClass3D(Star);
var doc2:DisplayObjectClass3D = new DisplayObjectClass3D(Circle);

к этому:

1
2
3
4
var mat1:MovieParticleMaterial = new MovieParticleMaterial(new Star());
var mat2:MovieParticleMaterial = new MovieParticleMaterial(new Circle());
var doc1:PV3DParticle = new PV3DParticle([mat1]);
var doc2:PV3DParticle = new PV3DParticle([mat2]);

Как вы, возможно, уже знаете, класс MovieParticleMaterial позволяет нам использовать экранные объекты для отображения частиц в Papervision3D. Мы создаем экземпляр Star и Circle для использования в качестве материала частиц. Инициализатор PV3DParticle занимает место инициализатора DisplayObjectClass3D; его конструктор принимает массив параметров, которые все будут добавлены в объект Particles.

Это все, что мы должны сделать в отношении излучателя. Далее мы изменим класс документа.


Целевой контейнер для нашего рендерера больше не является объектом Sprite. Вместо этого мы собираемся создать частицы в объекте Particles. Нам придется переключить тип рендерера с DisplayObjectRenderer3D на PV3DParticleRenderer.

Объявите следующие переменные для объектов, связанных с Papervision3D:

1
2
3
4
5
6
private var scene:SceneObject3D;
private var particles:Particles;
private var camera:Camera3D;
private var origin:DisplayObject3D;
private var renderEngine:BasicRenderEngine;
private var viewport:Viewport3D;

Код в конструкторе класса документа теперь:

01
02
03
04
05
06
07
08
09
10
initPV3D();
 
clock = new SteadyClock(PARTICLE_RATE);
emitter = new StarEmitter(clock);
 
renderer = new PV3DParticleRenderer(particles);
renderer.addEmitter(emitter);
 
pause_btn.addEventListener(MouseEvent.CLICK, togglePause);
addEventListener(Event.ENTER_FRAME, mainLoop);

Метод initPV3D () устанавливает среду Papervision3D. Вот код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private function initPV3D():void {
     
    //create the scene
    scene = new SceneObject3D();
     
    //create the Particles object
    particles = new Particles();
     
    //create the camera and initialize its position
    camera = new Camera3D();
    camera.position.x = 0;
    camera.position.y = 0;
    camera.position.z = -CAMERA_RADIUS;
     
    //create a DO3D representing the origin
    origin = new DisplayObject3D();
    origin.x = origin.y = origin.z = 0;
     
    //point the camera to the origin
    camera.target = origin;
     
    scene.addChild(origin);
    scene.addChild(particles);
     
    //create the render engine and viewport
    renderEngine = new BasicRenderEngine();
    viewport = new Viewport3D(640, 400);
     
    //add the viewport to the stage
    addChild(viewport);
    //add the pause button again so that it is on top of the viewport
    addChild(pause_btn);
}

Теперь Stardust обновляет только свойства 3D-объектов; Рендеринг Papervision3D берет на себя ответственность за рендеринг. Вот как выглядит наш новый основной цикл:

1
2
3
4
5
private function mainLoop(e:Event):void {
    updateCamera();
    emitter.step();
    renderEngine.renderScene(scene, camera, viewport);
}

Теперь, когда мы используем камеру Papervision3D, нам также придется изменить метод updateCamera ():

01
02
03
04
05
06
07
08
09
10
11
12
private function updateCamera():void {
    var theta:Number = 0.02 * (mouseX — 320);
    var phi:Number = 0.02 * (mouseY — 200);
    phi = StardustMath.clamp(phi, -StardustMath.HALF_PI, StardustMath.HALF_PI);
     
    var x:Number = CAMERA_RADIUS * Math.cos(theta) * Math.cos(phi);
    var y:Number = -CAMERA_RADIUS * Math.sin(phi);
    var z:Number = CAMERA_RADIUS * Math.sin(theta) * Math.cos(phi);
    camera.x = x;
    camera.y = y;
    camera.z = z;
}

Хорошо, мы успешно переключились с родного 3D движка Stardust на Papervision3D. Теперь давайте проверим результат. Обратите внимание на эффект пикселизации частиц. Это потому, что Papervision3D сначала рисует векторные объекты в растровые изображения, прежде чем использовать их в качестве материалов частиц.

Вы можете найти весь исходный код для этого в папке «02 — Papervision3D Particles».


До сих пор мы работали с «2D рекламными щитами» — плоскими объектами, такими как бумага. Можно создавать «настоящие» объекты 3D-частиц, например объекты DisplayObject3D Papervision3D. Нам просто нужно использовать другой инициализатор. Теперь давайте перейдем к заключительной части этого урока. Мы создадим частицы красного и синего куба.


Мы собираемся изменить инициализатор относительно внешнего вида частиц в последний раз.

Перед этим объявите переменную LightObject3D в классе эмиттера. Мы будем использовать FlatShadeMaterial для объектов DisplayObject3D, для которых требуется источник света. Кроме того, объявите следующие константы — мы будем использовать их в качестве параметров для FlastShadeMaterial и для определения размеров кубов:

1
2
3
4
5
6
7
public var light:LightObject3D;
 
private static const LIGHT_COLOR_1:uint = 0xCC3300;
private static const LIGHT_COLOR_2:uint = 0x006699;
private static const AMBIENT_COLOR_1:uint = 0x881100;
private static const AMBIENT_COLOR_2:uint = 0x002244;
private static const CUBE_SIZE:Number = 15;

Теперь измените следующий код в классе эмиттера:

1
2
3
4
var mat1:MovieParticleMaterial = new MovieParticleMaterial(new Star());
var mat2:MovieParticleMaterial = new MovieParticleMaterial(new Circle());
var doc1:PV3DParticle = new PV3DParticle([mat1]);
var doc2:PV3DParticle = new PV3DParticle([mat2]);

к этому:

1
2
3
4
5
6
7
8
9
light = new LightObject3D();
var mat1:FlatShadeMaterial = new FlatShadeMaterial(light, LIGHT_COLOR_1, AMBIENT_COLOR_1);
var mat2:FlatShadeMaterial = new FlatShadeMaterial(light, LIGHT_COLOR_2, AMBIENT_COLOR_2);
var matList1:MaterialsList = new MaterialsList({all:mat1});
var matList2:MaterialsList = new MaterialsList({all:mat2});
var params1:Array = [matList1, CUBE_SIZE, CUBE_SIZE, CUBE_SIZE];
var params2:Array = [matList2, CUBE_SIZE, CUBE_SIZE, CUBE_SIZE];
var doc1:PV3DDisplayObject3DClass = new PV3DDisplayObject3DClass(Cube, params1);
var doc2:PV3DDisplayObject3DClass = new PV3DDisplayObject3DClass(Cube, params2);

Внешний вид новой частицы будет инициализирован как красный и синий трехмерные кубы. Первый параметр конструктора для инициализатора PV3DDisplayObject3DClass — это класс, который мы хотим создать для частиц (поэтому здесь это класс Cube), а второй параметр — это массив параметров конструктора для этого класса Cube.


Раньше, поскольку мы работали с «2D билбордами», имело значение только вращение вокруг оси Z. Теперь, когда мы работаем с настоящими трехмерными объектами, нам нужно передать три ссылки на объекты Random в конструкторы Rotation3D и Omega3D, по одной для каждой оси.

Измените следующий код в классе эмиттера:

1
2
addInitializer(new Rotation3D(null, null, new UniformRandom(0, 180)));
addInitializer(new Omega3D(null, null, new UniformRandom(OMEGA_AVG, OMEGA_VAR)));

к этому:

1
2
3
4
var rotationRandom:UniformRandom = new UniformRandom(0, 180);
var omegaRandom:UniformRandom = new UniformRandom(OMEGA_AVG, OMEGA_VAR);
addInitializer(new Rotation3D(rotationRandom, rotationRandom, rotationRandom));
addInitializer(new Omega3D(omegaRandom, omegaRandom, omegaRandom));

На этот раз вместо использования объекта Particles в качестве контейнера для частиц мы используем DisplayObject3D в качестве контейнера. Объявите переменную для этого контейнера в классе документа:

1
private var container:DisplayObject3D;

Также нам понадобится рендерер другого типа для создания частиц в новом контейнере. Измените тип рендерера с PV3DParticleRenderer на PV3DDisplayObject3DRenderer. Код в конструкторе класса документа теперь должен выглядеть так:

01
02
03
04
05
06
07
08
09
10
initPV3D();
 
clock = new SteadyClock(PARTICLE_RATE);
emitter = new StarEmitter(clock);
 
renderer = new PV3DDisplayObject3DRenderer(container);
renderer.addEmitter(emitter);
 
pause_btn.addEventListener(MouseEvent.CLICK, togglePause);
addEventListener(Event.ENTER_FRAME, mainLoop);

В функции initPV3D () нам теперь нужно инициализировать переменную контейнера и добавить ее в сцену. Добавьте эти две строки в конец этой функции:

1
2
container = new DisplayObject3D();
scene.addChild(container);

В методе updateCamera () мы хотим, чтобы свет следовал за камерой, поэтому у нас будет иллюзия, что свет всегда «выстреливает» из наших глаз. Измените следующий код:

1
2
3
camera.x = x;
camera.y = y;
camera.z = z;

к этому:

1
2
3
emitter.light.x = camera.x = x;
emitter.light.y = camera.y = y;
emitter.light.z = camera.z = z;

Теперь источник света всегда находится в той же точке, что и камера.


Да, мы наконец-то закончили с этим уроком. Нет больше кодирования. Давайте посмотрим на наш конечный результат с причудливыми красными и синими 3D кубами!

Исходный код для этого можно найти в папке «Papervision3D DisplayObject3D».


Рабочий процесс создания эффектов 3D-частиц с помощью Stardust практически такой же, как и для 2D-эффектов. Вы просто выбираете другой набор инициализаторов, действий и средств визуализации. Stardust также поддерживает другие 3D-движки, включая ZedBox и ND3D . Использование практически одинаково. Вам просто нужно использовать другой набор инициализаторов и средств визуализации. Вы даже можете расширить классы Initializer, Action и Renderer для работы с любыми трехмерными движками, которые вам нравятся!

Теперь у вас есть основы, почему бы не вернуться к константам, созданным в шаге 6, и поиграть с ними, чтобы увидеть эффекты?

Я надеюсь, что этот учебник поможет вам лучше понять Stardust и сделает вас более знакомыми с рабочим процессом Stardust. Спасибо за прочтение!