Статьи

Управление движением частиц с помощью двигателя Stardust Particle Engine — Часть 1

Stardust Particle Engine предоставляет два основных подхода к свободному управлению движением частиц, а именно гравитационные поля и дефлекторы. Гравитационные поля — это векторные поля, которые влияют на ускорение частицы, а дефлекторы управляют как положением, так и скоростью частицы.

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

Предварительное знание основ использования Stardust необходимо для продолжения чтения этого учебника. Если вы не знакомы с Stardust, перед тем, как продолжить, вы можете ознакомиться с моим предыдущим уроком на эту тему, « Стреляй по звездам с помощью двигателя частиц Stardust» .


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


Пришло время вспомнить о физике в старшей школе. Помните основную кинематику? Все дело в смещении, которое является просто причудливым способом сказать «положение» и его отношение ко времени. В рамках данного урока нам достаточно просто понять тему.

Смещение означает текущую позицию объекта. В этом уроке «объекты», с которыми мы в основном имеем дело, — это частицы в 2D-пространстве. В 2D-пространстве смещение объекта представляется 2D-вектором.

Скорость объекта обозначает, как быстро изменяется положение объекта и направление изменения. Скорость объекта в 2D-пространстве также представлена ​​2D-вектором. X- и y-компоненты вектора представляют направление изменения положения, а абсолютное значение вектора обозначает скорость объекта, то есть, насколько быстро объект движется.

Ускорение зависит от скорости, а скорость — от смещения. Ускорение объекта обозначает, насколько быстро изменяется скорость объекта и направление изменения. Так же, как скорость объекта в 2D-пространстве, ускорение объекта представлено 2D-вектором.


Еще одна вещь, которую стоит упомянуть, это понятие векторных полей. Вы можете в основном рассматривать векторное поле как функцию, которая принимает вектор в качестве входного и выводит другой вектор. Гравитационные поля — это своего рода векторные поля, которые принимают векторы положения в качестве входных и выходных векторов ускорения. Например, в физическом моделировании, как правило, равномерная гравитация, направленная вниз, применяется ко всем объектам; в этом случае гравитационное поле, представляющее гравитацию, является векторным полем, которое выводит постоянный вектор (направленный вниз), независимо от того, каковы входные векторы положения.

Это визуализированный график векторного поля. Выходной вектор данного входного вектора (1, 2) равен (0,5, 0,5), тогда как выходной сигнал входа (4, 3) равен (-0,5, 0,5).

Класс Field в Stardust представляет двухмерное векторное поле, а его класс 3D, Field3D представляет трехмерное векторное поле. В этом уроке мы сосредоточимся только на двухмерных векторных полях.

Метод Field.getMotionData() принимает в качестве параметра объект Particle2D , содержащий двумерную информацию о положении и скорости частицы. Этот метод возвращает объект MotionData2D , двумерный вектор «объект значения», который состоит из x- и y-компонентов. В сочетании с действием Gravity объект Field можно использовать в качестве гравитационного поля, выход которого используется для управления скоростью частиц.

Это все для нашего резюме физики в средней школе. Теперь пришло время для настоящего Stardust.


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

1
2
3
4
5
6
7
8
//create a uniform vector field pointing downward (remember positive y-coordinate means «down» in Flash)
var field:Field = new UniformField(0, 1);
 
//create a gravity action
var gravity:Gravity = new Gravity();
 
//add the field to the gravity action
gravity.addField(field);

Это действие Gravity теперь готово для использования так же, как и любые другие обычные действия Stardust.

1
2
//add the gravity action to an emitter
emitter.addAction(gravity);

Мы собираемся создать эффект ветреного дождя, используя действие Gravity .


Создайте новый документ Flash, выберите темный цвет для фона, нарисуйте дождевую каплю на сцене и преобразуйте дождевую каплю в символ фрагмента ролика, экспортированный для ActionScript с именем класса «Дождевая капля». Удалите экземпляр дождевой капли на сцене позже.

Теперь мы собираемся создать файл AS для класса документа и файл AS для источника дождя. Я не буду объяснять код здесь, так как я уже рассмотрел основы использования Stardust в своем предыдущем уроке. Если вы не знакомы с Stardust или вам нужно немного освежиться, я настоятельно рекомендую вам прочитать мой предыдущий учебник, прежде чем двигаться дальше.

Это класс документа.

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
package {
    import flash.display.*;
    import flash.events.*;
    import idv.cjcat.stardust.common.emitters.*;
    import idv.cjcat.stardust.common.renderers.*;
    import idv.cjcat.stardust.twoD.renderers.*;
 
    public class WindyRain extends Sprite {
         
        private var emitter:Emitter;
        private var renderer:Renderer;
         
        public function WindyRain() {
            emitter = new RainEmitter();
            renderer = new DisplayObjectRenderer(this);
            renderer.addEmitter(emitter);
             
            addEventListener(Event.ENTER_FRAME, mainLoop);
        }
         
        private function mainLoop(e:Event):void {
            emitter.step();
        }
    }
}

И это класс эмиттера, используемый в классе документа.

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
package {
    import idv.cjcat.stardust.common.clocks.*;
    import idv.cjcat.stardust.common.initializers.*;
    import idv.cjcat.stardust.common.math.*;
    import idv.cjcat.stardust.twoD.actions.*;
    import idv.cjcat.stardust.twoD.emitters.*;
    import idv.cjcat.stardust.twoD.initializers.*;
    import idv.cjcat.stardust.twoD.zones.*;
     
    public class RainEmitter extends Emitter2D {
         
        public function RainEmitter() {
            super(new SteadyClock(1));
             
            //initializers
            addInitializer(new DisplayObjectClass(Raindrop));
            addInitializer(new Position(new RectZone(-300, -40, 940, 20)));
            addInitializer(new Velocity(new RectZone( -0.5, 2, 1, 3)));
            addInitializer(new Mass(new UniformRandom(2, 1)));
            addInitializer(new Scale(new UniformRandom(1, 0.2)));
             
            //actions
            addAction(new Move());
            addAction(new Oriented(1, 180));
            addAction(new DeathZone(new RectZone( -300, -40, 960, 480), true));
        }
    }
}

Вот как выглядит наш текущий прогресс.


Теперь мы создадим эффект дождя, добавив равномерное гравитационное поле, которое «тянет» капли дождя вправо. Добавьте следующий код в конструктор класса RainEmitter .

01
02
03
04
05
06
07
08
09
10
11
12
//create a uniformfield that always returns (0.5, 0)
var field:Field = new UniformField(0.5, 0);
 
//take particle mass into account
field.massless = false;
 
//create a gravity action and add the field to it
var gravity:Gravity = new Gravity();
gravity.addField(field);
 
//add the gravity action to the emitter
addAction(gravity);

Обратите внимание, что мы установили для свойства Field.massless значение false, которое по умолчанию равно true Когда установлено значение true, это свойство заставляет поля действовать как обычные гравитационные поля, воздействуя одинаково на все объекты независимо от их массы. Однако, когда свойство установлено в false, масса частиц учитывается: частицы с большей массой меньше подвержены воздействию поля, тогда как частицы с меньшей массой влияют больше. Вот почему мы использовали инициализатор Mass в нашем предыдущем коде эмиттера, чтобы добавить некоторую случайность в эффект дождя.

Протестируйте фильм еще раз, и вот как выглядит наш результат. Дождевые капли теперь подвержены воздействию гравитационного поля и все «тянут» вправо.


Теперь мы собираемся заменить объект UniformField объект BitmapField , возвращая векторные поля на основе цветовых каналов растрового изображения, чтобы создать эффект турбулентности. Если вы некоторое время работали с ActionScript, вы, возможно, ожидаете использовать метод BitmapData.perlinNoise() думая о турбулентности, и это именно то, что мы собираемся сделать.

Вот как выглядит растровое изображение шума Перлина. Шум Perlin — это отличный алгоритм для генерации шума для моделирования турбулентности, волны воды, облаков и т. Д. Более подробную информацию о шуме Perlin можно найти здесь .

Вам может быть интересно, если мы собираемся использовать метод BitmapData.perlinNoise() для генерации битовой карты с шумом перлина, как мы собираемся использовать эту битовую карту в качестве векторного поля? Ну, для этого и BitmapField класс BitmapField . Он принимает растровое изображение и преобразует его в векторное поле.

Допустим, у нас есть поле растрового изображения, для которого канал X установлен на красный, а канал Y — на зеленый . Это означает, что когда поле принимает входной вектор, скажем, (2, 3), оно просматривает пиксель растрового изображения в точке (2, 3), и компонент X выходного вектора определяется из красного канала пикселя, а компонент Y равен определяется по зеленому каналу. Когда компоненты входного вектора не являются целыми числами, они округляются в первую очередь.

Значение цветового канала варьируется от 0 до 255, 127 — среднее. Значение меньше 127 считается отрицательным в поле битовой карты, а значение больше 127 принимается положительным. Ноль — самое отрицательное число, а 255 — самое положительное . Например, если у нас есть пиксель с цветом 0xFF0000 в шестнадцатеричном представлении, то есть красный канал со значением 255 и зеленый канал с 0, то выходное поле битового поля для этого пикселя будет вектором с компонентом X максимально положительного числа и компонент Y максимально отрицательного числа, где это максимально положительное / отрицательное число , или максимальное число , является специфическим для поля битовой карты. Чтобы быть более точным, вот формула преобразования пикселя в вектор.


Создайте новый документ Flash. Нарисуйте стрелку на сцене, преобразуйте ее в символ с именем «Стрелка» и экспортируйте для ActionScript.

Создайте файл AS для класса документа. Это почти так же, как и в предыдущем примере.

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
package {
    import flash.display.*;
    import flash.events.*;
    import idv.cjcat.stardust.common.emitters.*;
    import idv.cjcat.stardust.common.renderers.*;
    import idv.cjcat.stardust.twoD.renderers.*;
 
    public class Turbulence extends Sprite {
         
        private var emitter:Emitter;
        private var renderer:Renderer;
         
        public function Turbulence() {
            emitter = new ArrowEmitter();
            renderer = new DisplayObjectRenderer(this);
            renderer.addEmitter(emitter);
             
            addEventListener(Event.ENTER_FRAME, mainLoop);
        }
         
        private function mainLoop(e:Event):void {
            emitter.step();
        }
    }
}

Создайте еще один файл AS для нашего класса эмиттера.

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
package {
    import idv.cjcat.stardust.common.actions.*;
    import idv.cjcat.stardust.common.clocks.*;
    import idv.cjcat.stardust.common.initializers.*;
    import idv.cjcat.stardust.common.math.*;
    import idv.cjcat.stardust.twoD.actions.*;
    import idv.cjcat.stardust.twoD.emitters.*;
    import idv.cjcat.stardust.twoD.initializers.*;
    import idv.cjcat.stardust.twoD.zones.*;
     
    public class ArrowEmitter extends Emitter2D {
         
        public function ArrowEmitter() {
            super(new SteadyClock(1));
             
            //initializers
            addInitializer(new DisplayObjectClass(Arrow));
            addInitializer(new Life(new UniformRandom(50, 10)));
            addInitializer(new Position(new SinglePoint(320, 200)));
            addInitializer(new Velocity(new LazySectorZone(3, 2)));
            addInitializer(new Mass(new UniformRandom(2, 1)));
            addInitializer(new Scale(new UniformRandom(1, 0.2)));
             
            //actions
            addAction(new Age());
            addAction(new DeathLife());
            addAction(new Move());
            addAction(new Oriented());
            addAction(new ScaleCurve(10, 10));
        }
    }
}

Текущий прогресс выглядит следующим образом.


Добавьте следующий код, который создает растровые данные шума Перлина 640 на 480 в конструкторе эмиттера. Подробные пояснения по каждому параметру метода BitmapData.perlinNoise() вы можете найти в этой документации . Для простоты следующий код создает битовый массив шума Перлина с «октавами» размером примерно 50X50, а шум состоит из каналов красного и зеленого цветов.

1
2
3
//create a Perlin noise bitmap data
var noise:BitmapData = new BitmapData(640, 400);
noise.perlinNoise(50, 50, 1, 0, true, true, 1 | 2);

Затем создайте объект BitmapField и назначьте ему растровые данные с помощью метода update() . Тогда остальная часть кода относительно действия Gravity точно такая же, как и в предыдущем примере.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
//create a uniformfield that always returns (0.5, 0)
var field:BitmapField = new BitmapField();
field.channelX = 1;
field.channelY = 2;
field.max = 1;
field.update(noise);
 
//take particle mass into account
field.massless = false;
 
//create a gravity action and add the field to it
var gravity:Gravity = new Gravity();
gravity.addField(field);
 
//add the gravity action to the emitter
addAction(gravity);

Теперь протестируйте фильм еще раз, и вы увидите, что наши летающие стрелы сейчас испытывают турбулентность.


Мы использовали UniformField и BitmapField предоставленные Stardust, и теперь мы собираемся создать наши собственные настраиваемые поля, расширяя класс Field и переопределяя метод getMotionData2D() .

getMotionData2D принимает параметр Particle2D качестве входных данных, который содержит информацию вектора положения частицы, и это входные данные для поля. Метод возвращает объект MotionData2D , представляющий выходные данные поля. Это все, что вам нужно знать для создания настраиваемого поля. Давайте создадим настраиваемое вихревое поле.

Ниже приведен визуализированный график нашего вихревого поля. Довольно понятно, почему оно называется вихревым полем.

Вот формула для нашего вихревого поля.

И класс вихревого поля так же прост, как код ниже. Мы использовали класс Vec2D чтобы выполнить всю грязную работу: Vec2D вектор на 90 градусов по часовой стрелке и установить абсолютное значение вектора. Затем мы MotionData2D x- и y-компоненты MotionData2D объект MotionData2D , который имеет возвращаемый тип объекта.

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
package {
    import idv.cjcat.stardust.twoD.fields.*;
    import idv.cjcat.stardust.twoD.geom.*;
    import idv.cjcat.stardust.twoD.particles.*;
     
    public class VortexField extends Field {
         
        public var centerX:Number;
        public var centerY:Number;
        public var strength:Number;
         
        public function VortexField(centerX:Number = 0, centerY:Number = 0, strength:Number = 1) {
            this.centerX = centerX;
            this.centerY = centerY;
            this.strength = strength;
        }
         
        override protected function calculateMotionData2D(particle:Particle2D):MotionData2D {
            var dx:Number = particle.x — centerX;
            var dy:Number = particle.y — centerY;
            var vec:Vec2D = new Vec2D(dx, dy);
            vec.length = strength;
            vec.rotateThis(90);
             
            return new MotionData2D(vec.x, vec.y);
        }
    }
}

Теперь, когда у нас есть настраиваемое поле, давайте проверим его в следующем примере.


Этот пример служит в качестве тест-драйва для нашего вихревого векторного поля. Это так же просто, как заменить поле новым. Измените следующий код из предыдущего примера из этого

1
2
3
4
5
6
//create a uniformfield that always returns (0.5, 0)
var field:BitmapField = new BitmapField();
field.channelX = 1;
field.channelY = 2;
field.max = 1;
field.update(noise);

к этому

1
2
3
4
5
//create a vortex field centered at (320, 200) with strength 1
var field:VortexField = new VortexField();
field.centerX = 320;
field.centerY = 200;
field.strength = 1;

И мы сделали. Вы можете проверить фильм и увидеть эффект вихря. Сладкий!


Вы видели, как использовать действие Gravity и класс Field чтобы влиять на скорость частиц. И вы узнали, как создавать свои собственные векторные поля, которые будут использоваться в качестве гравитационных полей. Следующая часть этого урока покажет вам, как использовать дефлекторы для одновременного управления положением и скоростью частиц.

Большое спасибо за чтение!