Введение в программирование трехмерных игр с DX9

         

Частицы и их атрибуты



14.1.3. Частицы и их атрибуты

У частиц может быть множество других атрибутов, помимо местоположения и цвета; например, у каждой частицы может быть своя скорость. Однако, эти дополнительные атрибуты не нужны для визуализации частицы. Соответственно мы храним данные для визуализации частицы и дополнительные атрибуты в разных структурах. Создавая, уничтожая и обновляя частицы мы работаем с их атрибутами; затем, когда все готово к визуализации, мы копируем местоположение и цвет частицы в структуру Particle.

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

struct Attribute { D3DXVECTOR3 _position; D3DXVECTOR3 _velocity; D3DXVECTOR3 _acceleration; float _lifeTime; float _age; D3DXCOLOR _color; D3DXCOLOR _colorFade; bool _isAlive; };

_position — Местоположение частицы в мировом пространстве.

_velocity — Скорость частицы, обычно измеряемая в условных единицах в секунду.

_acceleration — Ускорение частицы, обычно измеряемое в условных единицах за секунду.

_lifeTime — Сколько времени должно пройти до гибели частицы. Например, мы можем указать, что частицы образующие лазерный луч пропадают через указанный период времени.

_age — Текущий возраст частицы.

_color — Цвет частицы.

_colorFade — Как цвет частицы меняется с течением времени.

_isAlive — True если частица жива, false если она погибла.



Частицы и точечные спрайты


Частицы— это очень маленькие объекты, которые обычно математически моделируются как точки. Из этого следует, что примитивы точек (D3DPT_POINTLIST из D3DPRIMITIVETYPE) стоят в первых строчках списка кандидатов на отображение частиц. Однако примитивы точек визуализируются как единственная точка. Это не предоставляет нам достаточной гибкости, поскольку могут требоваться частицы разного размера и текстуры частиц могут даже наноситься на карту. даже вся карта может быть тексурирована этими частицами. До Direct3D 8.0 способ обойти накладываемые на примитивы точек ограничения состоял в том, чтобы не использовать их вообще. Вместо этого для отображения частиц программисты использовали щиты (billboard). Щит — это прямоугольник, который мировая матрица всегда ориентирует так, чтобы лицевой стороной он был обращен к камере.

В Direct3D 8.0 появился специальный точечный примитив, называемый точечный спрайт (point sprite), который замечательно подходит для реализации систем частиц. В отличие от обычных точечных примитивов точечному спрайту может быть назначена текстура, которая может изменять свой размер. В отличие от щита мы можем описать точечный спрайт с помощью одной точки, что экономит память и время процессора, ведь нам надо обрабатывать одну вершину, а не четыре как у щита (прямоугольника).



Формат структуры



14.1.1. Формат структуры



Для описния местоположения и цвета наших частиц мы будем использовать следующий формат вершин:

struct Particle { D3DXVECTOR3 _position; D3DCOLOR _color; static const DWORD FVF; }; const DWORD Particle::FVF = D3DFVF_XYZ | D3DFVF_DIFFUSE;

В структуре просто хранятся местоположение вершины и ее цвет. В зависимости от требований вашего приложения вы можете хранить также и набор координат текстур. Текстурирование точечных спрайтов мы рассмотрим в следующем разделе.

В структуру Particle можно добавить переменную с плавающей точкой, задающую размер частицы. Для этого к описанию настраиваемого формата вершин следует добавить флаг D3DFVF_PSIZE. Если частица может иметь собственный размер, мы сможем реализовать множество эффектов, основанных на изменении размеров отдельных частиц. Однако большинство видеокарт не поддерживают такой способ управления размером частиц, поэтому мы не будем обсуждать его. (Чтобы убедиться, что ваша видеокарта поддерживает эту возможность проверьте флаг D3DFVFCAPS_PSIZE в члене FVFCaps структуры D3DCAPS9.) Вместо этого мы будем управлять размером частиц с помощью режимов визуализации, как показано ниже. Вот пример структуры данных вершины с членом, определяющим размер:

struct Particle { D3DXVECTOR3 _position; D3DCOLOR _color; float _size; static const DWORD FVF; }; const DWORD Particle::FVF = D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_PSIZE;

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



Системы частиц


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

Цели

Изучить атрибуты, которые мы назначаем частицы и то, как представить частицу в Direct3D

Разработать гибкий базовый класс системы частиц, включающий атрибуты и методы общие для всех систем частиц.

Создать модели трех систем частиц: снегопада, фейерверка и следа от снаряда.



Хаотичность


14.2.2. Хаотичность

В системах частиц есть своего рода хаотичность. Например, моделируя снегопад мы не хотим, чтобы все снежинки падали абсолютно одинаково. Нам нужно чтобы они падали похожим образом, а не абсолютно одинаково. Чтобы облегчить реализацию хаотичности, необходимую для систем частиц, мы добавляем в файлы d3dUtility.h/cpp две функции.

Первая функция возвращает случайное число с плавающей точкой, находящееся в диапазоне [lowBound, highBound]:

float d3d::GetRandomFloat(float lowBound, float highBound) { if(lowBound >= highBound) // неправильные параметры return lowBound;

// Получаем случайное число в диапазоне [0, 1] float f = (rand() % 10000) * 0.0001f;

// Возвращаем число из диапазона [lowBound, highBound] return (f * (highBound - lowBound)) + lowBound; }

Следующая функция возвращает случайный вектор в параллелепипеде, заданном двумя углами min и max.

void d3d::GetRandomVector( D3DXVECTOR3* out, D3DXVECTOR3* min, D3DXVECTOR3* max) { out->x = GetRandomFloat(min->x, max->x); out->y = GetRandomFloat(min->y, max->y); out->z = GetRandomFloat(min->z, max->z); }

ПРИМЕЧАНИЕ

Не забывайте об инициализации генератора случайных чисел с помощью функции srand().

и гибкий способ отображения частиц.


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

Компоненты системы частиц


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

Хотя различные системы частиц ведут себя по-разному, мы можем выполнить обобщение и выделить некоторые базовые свойства, которые используются всеми системами частиц. Мы поместим эти общие свойства в абстрактный базовый класс PSystem, который будет родителем для классов конкретных систем частиц. Давайте теперь взглянем на класс PSystem:

class PSystem { public: PSystem(); virtual ~PSystem();

virtual bool init(IDirect3DDevice9* device, char* texFileName); virtual void reset(); virtual void resetParticle(Attribute* attribute) = 0; virtual void addParticle(); virtual void update(float timeDelta) = 0;

virtual void preRender(); virtual void render(); virtual void postRender();

bool isEmpty(); bool isDead(); protected: virtual void removeDeadParticles();

protected: IDirect3DDevice9* _device; D3DXVECTOR3 _origin; d3d::BoundingBox _boundingBox; float _emitRate; float _size; IDirect3DTexture9* _tex; IDirect3DVertexBuffer9* _vb; std::list<Attribute> _particles; int _maxParticles;

DWORD _vbSize; DWORD _vbOffset; DWORD _vbBatchSize; };

Начнем с членов данных:

_origin — Базовая точка системы. Это то место, откуда появляются частицы системы.

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

_emitRate — Частота добавления новых частиц к системе. Обычно измеряется в частицах в секунду.

_size — Размер всех частиц системы.

_particles — Список, содержащий атрибуты частиц системы. Мы работаем с этим списком при создании, уничножении и обновлении частиц. Когда мы готовы к рисованию частиц, мы копируем часть узлов списка в буфер вершин и рисуем частицы. Затем мы копируем следующий блок и рисуем частицы. Эти действия повторяются до тех пор, пока не будут нарисованы все частицы. Это крайне упрощенное описание; подробно процесс рисования будет рассмотрен в разделе 14.2.1.

_maxParticles — Максимальное количество частиц, которое может быть в системе одновременно. Если, к примеру, частицы создаются быстрее чем удаляются, может получиться, что у вас будет огромное количество частиц, что приведет к неработоспособности программы. Данный член позволяет избежать такого развития событий.

_vbSize — Количество частиц, которое может быть помещено в буфер вершин одновременно. Это значение не зависит от количества частиц в конкретной системе частиц.

ПРИМЕЧАНИЕ

Члены данных _vbOffset и _vbBatchSize используются при визуализации системы частиц. Мы отложим их обсуждение до раздела 14.2.1. Методы класса:

PSystem/~PSystem — Конструктор инициализирует значения по умолчанию, а деструктор освобождает интерфейсы устройства (буфер вершин, текстуры).

init — Этот метод выполняет зависящую от устройства Direct3D инициализацию, такую как создание буфера вершин для хранения точечных спрайтов и создание текстуры. При создании буфера вершин указываются несколько флагов о которых мы говорили, но до настоящего момента еще ни разу не использовали:

hr = device->CreateVertexBuffer( _vbSize * sizeof(Particle), D3DUSAGE_DYNAMIC | D3DUSAGE_POINTS | D3DUSAGE_WRITEONLY, Particle::FVF, D3DPOOL_DEFAULT, &_vb, 0);

Обратите внимание, что мы используем динамический буфер вершин. Это вызвано тем, что мы должны в каждом кадре обновлять данные частиц, что требует доступа к памяти буфера вершин.


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

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

Обратите внимание, что размер буфера вершин задан переменной _vbSize и не имеет ничего общего с количеством частиц в системе. То есть, значение _vbSize очень редко равно количеству частиц в системе. Это вызвано тем, что мы визуализируем систему частиц по частям, а не всю сразу. Процесс визуализации мы исследуем в разделе 14.2.1.

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

reset — Этот метод сбрасывает значения атрибутов у каждой частицы системы:

void PSystem::reset() { std::list<Attribute>::iterator i; for(i = _particles.begin(); i != _particles.end(); i++) { resetParticle(&(*i)); } }

resetParticle — Этот метод сбрасывает значения атрибутов одной частицы. То, как именно должен выполняться сброс атрибутов, зависит от параметров конкретной системы частиц. Следовательно, мы делаем этот метод абстрактным и он должен быть реализован в производном классе.

addParticle — Этот метод добавляет частицу к системе. Он использует метод resetParticle для инициализации частицы перед ее добавлением к списку:

void PSystem::addParticle() { Attribute attribute;

resetParticle(&attribute);

_particles.push_back(attribute); }

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

render — Данный метод отображает все частицы системы. Его реализация достаточно сложна и мы отложим ее обсуждение до раздела 14.2.1.

preRender — Применяется для установки начальных режимов визуализации, которые должны быть заданы перед визуализацией.


Поскольку они могут меняться в зависимости от конкретной системы частиц, мы делаем этот метод виртуальным. Предлагаемая по умолчанию реализация выглядит так:

void PSystem::preRender() { _device->SetRenderState(D3DRS_LIGHTING, false); _device->SetRenderState(D3DRS_POINTSPRITEENABLE, true); _device->SetRenderState(D3DRS_POINTSCALEENABLE, true); _device->SetRenderState(D3DRS_POINTSIZE, d3d::FtoDw(_size)); _device->SetRenderState(D3DRS_POINTSIZE_MIN, d3d::FtoDw(0.0f));

// Управление изменением размера частицы // в зависимости от расстояния до нее _device->SetRenderState(D3DRS_POINTSCALE_A, d3d::FtoDw(0.0f)); _device->SetRenderState(D3DRS_POINTSCALE_B, d3d::FtoDw(0.0f)); _device->SetRenderState(D3DRS_POINTSCALE_C, d3d::FtoDw(1.0f));

// Для текстуры используется альфа-смешивание _device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE); _device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); _device->SetRenderState(D3DRS_ALPHABLENDENABLE, true); _device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); _device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); }

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

postRender — Используется для восстановления режимов визуализации, которые изменила данная система частиц. Поскольку режимы меняются в зависимости от конкретной системы частиц, этот метод виртуальный. По умолчанию используется следующая реализация:

void PSystem::postRender() { _device->SetRenderState(D3DRS_LIGHTING, true); _device->SetRenderState(D3DRS_POINTSPRITEENABLE, false); _device->SetRenderState(D3DRS_POINTSCALEENABLE, false); _device->SetRenderState(D3DRS_ALPHABLENDENABLE, false); }


isEmpty — Метод возвращает true, если в системе нет ни одной частицы и false в ином случае.

isDead — Метод возвращает true если все частицы в системе мертвы и false, если хотя бы одна частица жива. Обратите внимание что если все частицы мертвы, это не значит, что система частиц пуста. В пустой системе нет ни мертвых ни живых частиц. Если система мертвая, это значит, что в ней есть частицы, но все они помечены как мертвые.

removeDeadParticles — Метод перебирает элементы списка атрибутов _particle и удаляет из него все частицы, которые отмечены как мертвые:

void PSystem::removeDeadParticles() { std::list::iterator i; i = _particles.begin(); while( i != _particles.end() ) { if( i->_isAlive == false ) { // стирание возвращает номер следующего элемента, // поэтому самостоятельно увеличивать счетчик не надо i = _particles.erase(i); } else { i++; // следующий элемент списка } } }

ПРИМЕЧАНИЕ

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


Окно программы Firework



Рисунок 14.3. Окно программы Firework


Определение класса системы Firework выглядит следующим образом:

class Firework : public PSystem { public: Firework(D3DXVECTOR3* origin, int numParticles); void resetParticle(Attribute* attribute); void update(float timeDelta); void preRender(); void postRender(); };

Конструктор получает указатель на базовую точку системы и количество частиц в системе. В данном случае базовая точка— это точка в которой взрывается фейерверк.

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

void Firework::resetParticle(Attribute* attribute) { attribute->_isAlive = true; attribute->_position = _origin;

D3DXVECTOR3 min = D3DXVECTOR3(-1.0f, -1.0f, -1.0f); D3DXVECTOR3 max = D3DXVECTOR3( 1.0f, 1.0f, 1.0f);

d3d::GetRandomVector( &attribute->_velocity, &min, &max);

// Нормализация для сферы D3DXVec3Normalize( &attribute->_velocity, &attribute->_velocity);

attribute->_velocity *= 100.0f;

attribute->_color = D3DXCOLOR( d3d::GetRandomFloat(0.0f, 1.0f), d3d::GetRandomFloat(0.0f, 1.0f), d3d::GetRandomFloat(0.0f, 1.0f), 1.0f);

attribute->_age = 0.0f; attribute->_lifeTime = 2.0f; // время жизни - 2 секунды }

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

void Firework::update(float timeDelta) { std::list<Attribute>::iterator i;

for(i = _particles.begin(); i != _particles.end(); i++) { // Обновляем только живые частицы if(i->_isAlive) { i->_position += i->_velocity * timeDelta;


i->_age += timeDelta;

if(i->_age > i->_lifeTime) // убиваем i->_isAlive = false; } } }

Система фейерверка при визуализации использует собственные коэффициенты смешивания. Кроме того, она еще и запрещает запись в буфер глубины. Мы можем легко изменить коэффициенты смешивания и запретить запись в буфер глубины путем переопределения методов PSystem::preRender и PSystem::postRender. Вот переопределенная реализация:

void Firework::preRender() { PSystem::preRender();

_device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE); _device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);

// Не записываем частицы в z-буфер _device->SetRenderState(D3DRS_ZWRITEENABLE, false); }

void Firework::postRender() { PSystem::postRender();

_device->SetRenderState(D3DRS_ZWRITEENABLE, true); }

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


Окно программы Laser (Particle Gun)



Рисунок 14.4. Окно программы Laser (Particle Gun)


Определение класса системы ParticleGun выглядит следующим образом:

class ParticleGun : public PSystem { public: ParticleGun(Camera* camera); void resetParticle(Attribute* attribute); void update(float timeDelta);

private: Camera* _camera; };

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

Метод resetParticle устанавливает координаты частицы равными текущим координатам камеры и задает вектор скорости частицы равным умноженному на сто вектору взгляда камеры. В результате «снаряд» будет выстрелен в направлении взгляда. Изображающей снаряд частице мы назначаем зеленый цвет.

void ParticleGun::resetParticle(Attribute* attribute) { attribute->_isAlive = true;

D3DXVECTOR3 cameraPos; _camera->getPosition(&cameraPos);

D3DXVECTOR3 cameraDir; _camera->getLook(&cameraDir);

// Получаем местоположение камеры attribute->_position = cameraPos; attribute->_position.y -= 1.0f; // смещаем позицию вниз, чтобы // казалось, что мы держим оружие // в руках

// Отправляем частицу в том направлении, куда смотрит камера attribute->_velocity = cameraDir * 100.0f;

// Назначаем зеленый цвет attribute->_color = D3DXCOLOR(0.0f, 1.0f, 0.0f, 1.0f);

attribute->_age = 0.0f; attribute->_lifeTime = 1.0f; // время жизни - 1 секунда }

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

void ParticleGun::update(float timeDelta) { std::list<Attribute>::iterator i;

for(i = _particles.begin(); i != _particles.end(); i++) { i->_position += i->_velocity * timeDelta;

i->_age += timeDelta;

if(i->_age > i->_lifeTime) // убиваем i->_isAlive = false; } removeDeadParticles(); }



Окно программы Snow


Рисунок 14.2. Окно программы Snow


Определение класса системы частиц Snow выглядит следующим образом:

class Snow : public PSystem { public: Snow(d3d::BoundingBox* boundingBox, int numParticles); void resetParticle(Attribute* attribute); void update(float timeDelta); };

ПРИМЕЧАНИЕ

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

Конструктор получает указатель на структуру данных ограничивающего параллелепипеда и количество частиц, которое должно быть в системе. Ограничивающий параллелепипед определяет объем пространства в котором будет падать снег. Если снежинки выходят за границы этого объема, они воскрешаются с новыми координатами. Таким образом система поддерживает постоянное количество активных частиц. Реализация конструктора выглядит следующим образом:

Snow::Snow(d3d::BoundingBox* boundingBox, int numParticles) { _boundingBox = *boundingBox; _size = 0.8f; _vbSize = 2048; _vbOffset = 0; _vbBatchSize = 512;

for(int i = 0; i < numParticles; i++) addParticle(); }

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

Метод resetParticle создает снежинку внутри ограничивающего параллелепипеда со случайными значениями координат X и Z, а значение координаты Y делает равным координате верха ограничивающего объема. Затем вектор скорости снежинки устанавливается таким образом, чтобы она падала вниз и при этом слегка смещалась влево. Помимо этого, снежинка окрашивается в белый цвет:

void Snow::resetParticle(Attribute* attribute) { attribute->_isAlive = true;

// Получить случайные значения координат X и Z снежинки d3d::GetRandomVector( &attribute->_position, &_boundingBox._min, &_boundingBox._max);

// Для высоты (координаты Y) случайное значение не нужно. // Падение снежинок всегда начинается с верха // ограничивающего параллелепипеда attribute->_position.y = _boundingBox._max.y;


// Снежинка падает вниз и слегка смещается влево attribute->_velocity.x = d3d::GetRandomFloat(0.0f, 1.0f) * -3.0f; attribute->_velocity.y = d3d::GetRandomFloat(0.0f, 1.0f) * -10.0f; attribute->_velocity.z = 0.0f;

// Все снежинки белые attribute->_color = d3d::WHITE; }

Метод update обновляет местоположение частиц, а затем проверяет не вышли ли какие-нибудь частицы за пределы ограничивающего объема системы. Если частица вышла за пределы ограничивающего параллелепипеда, она заново инициализируется и снова падает с самого верха.

void Snow::update(float timeDelta) { std::list<Attribute>::iterator i; for(i = _particles.begin(); i != _particles.end(); i++) { i->_position += i->_velocity * timeDelta;

// Точка вне ограничивающего объема? if(_boundingBox.isPointInside(i->_position) == false) { // Вышедшие за пределы объема частицы не // уничтожаются, а снова используются и // воскрешаются с новыми координатами resetParticle(&(*i)); } } }


Примеры систем частиц: снег, фейерверк, след снаряда

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

ПРИМЕЧАНИЕ

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

Разделенный на сегменты буфер вершин


Рисунок 14.1. Разделенный на сегменты буфер вершин


Теперь создайте глобальную переменную i= 0 для отслеживания того, с каким сегментом мы работаем.

Для каждого кадра:

Обновите все частицы.

Пока не будут визуализированы все живые частицы:

Если буфер вершин не заполнен, то:

Блокируем сегмент i с флагом D3DLOCK_NOOVERWRITE.

Копируем 500 частиц в сегмент i.

Если буфер вершин заполнен, то:

Возвращаемся к началу буфера вершин: i = 0.

Блокируем сегмент i с флагом D3DLOCK_DISCARD.

Копируем 500 частиц в сегмент i.

Визуализируем сегмент i.

Переходим к следующему сегменту: i++

ПРИМЕЧАНИЕ

Это очень упрощенное описание, но оно иллюстрирует идею. Предполагается, что у нас всегда есть 500 частиц, чтобы заполнить целый сегмент, хотя в действительности это условие не выполняется потому что мы постоянно создаем и уничтожаем частицы и их количество меняется от кадра к кадру. Предположим, что у нас осталось только 200 частиц для копирования и визуализации в текущем кадре. Поскольку 200 частиц недостаточно для заполнения буфера вершин, в коде мы обрабатываем такой сценарий как особый случай. Данный сценарий может иметь место только в том случае, когда в текущем кадре заполняется последний сегмент. Если сегмент не последний, это значит что у нас есть по крайней мере 500 частиц для перехода к следующему сегменту.

ПРИМЕЧАНИЕ

Вспомните, что наш буфер вершин динамический, и поэтому мы можем пользоваться преимуществами, предоставляемыми флагами динамической блокировки D3DLOCK_NOOVERWRITE и D3DLOCK_DISCARD. Эти флаги позволяют блокировать часть буфера вершин, которая не визуализируется, в то время как остальные части буфера вершин будут продолжать визуализироваться. Например, предположим, что мы визуализируем сегмент 0; используя флаг D3DLOCK_NOOVERWRITE мы можем заблокировать и заполнить сегмент 1 в то время, когда визуализируется сегмент 0. Это позволяет предотвратить простои визуализации, которые возникали бы в ином случае.
Данный подход более эффективен. Во-первых, мы сокращаем размер необходимого нам буфера вершин. Во-вторых, теперь центральный процессор и видеокарта работают в унисон; то есть мы копируем небольшую партию частиц в буфер вершин (работа центрального процессора), а затем мы визуализируем эту партию частиц (работа видеокарты). Затем мы копируем в буфер вершин следующую партию частиц и рисуем ее. Это продолжается до тех пор, пока не будут визуализированы все частицы. Как видите, видеокарта больше не простаивает, ожидая пока не будет заполнен весь буфер вершин.

Теперь мы обратим наше внимание на реализацию этой схемы визуализации. Чтобы облегчить визуализацию системы частиц с помощью данной схемы мы будем использовать следующие члены данных класса PSystem:

_vbSize — Количество частиц, которые одновременно могут храниться в нашем буфере вершин. Это значение не зависит от количества частиц в конкретной системе частиц.

_vbOffset — Переменная хранит смещение в буфере вершин (измеряемое в частицах, а не в байтах), начиная с которого мы должны выполнять копирование очередной партии частиц. Например, если первая партия частиц заняла элементы буфера вершин с номерами от 0 до 499, то копирование следующей партии должно начинаться со смещения 500.

_vbBatchSize — Количество частиц в партии.

Теперь мы представим вам код метода визуализации:

void PSystem::render() { if(!_particles.empty()) { // Установка режимов визуализации preRender(); _device->SetTexture(0, _tex); _device->SetFVF(Particle::FVF); _device->SetStreamSource(0, _vb, 0, sizeof(Particle));

// Если мы достигли конца буфера вершин, // возвращаемся к его началу if(_vbOffset >= _vbSize) _vbOffset = 0;

Particle* v = 0;

_vb->Lock( _vbOffset * sizeof(Particle), _vbBatchSize * sizeof(Particle), (void**)&v, _vbOffset ? D3DLOCK_NOOVERWRITE : D3DLOCK_DISCARD);

DWORD numParticlesInBatch = 0;

// // Пока все частицы не будут визуализированы // std::list<Attribute>::iterator i; for(i = _particles.begin(); i != _particles.end(); i++) { if(i->_isAlive) { // // Копируем партию живых частиц в // очередной сегмент буфера вершин // v->_position = i->_position; v->_color = (D3DCOLOR)i->_color; v++; // следующий элемент;



numParticlesInBatch++; // увеличиваем счетчик партий

// партия полная? if(numParticlesInBatch == _vbBatchSize) { // // Рисуем последнюю партию частиц, которая // была скопирована в буфер вершин. // _vb->Unlock(); _device->DrawPrimitive( D3DPT_POINTLIST, _vbOffset, _vbBatchSize); // // Пока партия рисуется, начинаем заполнять // следующую партию частиц. // // Увеличиваем смещение к началу следующей партии

_vbOffset += _vbBatchSize;

// Проверяем не вышли ли мы за пределы буфера вершин. // Если да, то возвращаемся к началу буфера. if(_vbOffset >= _vbSize) _vbOffset = 0;

_vb->Lock( _vbOffset * sizeof(Particle), _vbBatchSize * sizeof(Particle), (void**)&v, _vbOffset ? D3DLOCK_NOOVERWRITE : D3DLOCK_DISCARD);

numParticlesInBatch = 0; // обнуляем количество частиц в партии }//конец инструкции if }//конец инструкции if }//конец инструкции for

_vb->Unlock();

// Возможно, ПОСЛЕДНЯЯ партия частиц начала заполняться, // но не была визуализирована, потому что условие // (numParticlesInBatch == _vbBatchSize) не было выполнено. // Сейчас мы нарисуем эту последнюю частично заполненную партию частиц if( numParticlesInBatch ) { _device->DrawPrimitive( D3DPT_POINTLIST, _vbOffset, numParticlesInBatch); }

// Следующий блок _vbOffset += _vbBatchSize;

postRender(); }//конец инструкции if }// конец метода render()


Режимы визуализации точечных спрайтов



14.1.2. Режимы визуализации точечных спрайтов

Поведение точечных спрайтов в основном контролируется через режимы визуализации. Сейчас мы рассмотрим эти режимы.

D3DRS_POINTSPRITEENABLE— Логическое значение. Значение по умолчанию — false.

True указывает, что установленная в данный момент текстура накладывается на точечные спрайты целиком.

False указывает, что на точечный спрайт накладывается только тот тексель текстуры, который задан координатами текстуры точечного спрайта (если координаты текстуры присутствуют в структуре данных вершины точечного спрайта).

_device->SetRenderState(D3DRS_POINTSPRITEENABLE, true);

D3DRS_POINTSCALEENABLE — Логическое значение. Значение по умолчанию — false.

True указывает, что размер точки интерпретируется в единицах пространства вида. Единицы пространства вида просто ссылаются на трехмерную точку в пространстве камеры. В этом случае размер точечного спрайта масштабируется в зависимости от того, как далеко он находится. В результате, подобно всем другим объектам, частицы расположенные далеко от камеры будут выглядеть меньше, чем частицы, расположенные близко к камере.

False указывает, что размер точки интерпретируется в единицах экранного пространства. Единицы экранного пространства это отображаемые на экране пиксели. Так что если вы укажете false и, например, установите размер точечного спрайта равным 3, то он будет занимать на экране область размером 3 × 3 пикселя.

_device->SetRenderState(D3DRS_POINTSCALEENABLE, true);

D3DRS_POINTSIZE — Используется для задания размера точечных спрайтов. Значение интерпретируется либо как единицы пространства вида, либо как единицы экранного пространства, в зависимости от установленного значения режима D3DRS_POINTSCALEENABLE. Приведенный ниже фрагмент кода устанавливает размер точки равным 2.5 единицам:

_device->SetRenderState(D3DRS_POINTSIZE, d3d::FtoDw(2.5f));

Функция d3d::FtoDw — это вспомогательная функция, добавленная нами в файлы d3dUtility.h/cpp, которая выполняет приведение типа float к типу DWORD.
Нам приходится выполнять эту операцию потому что функция IDirect3DDevice9::SetRenderState ожидает значения типа DWORD а не float.

DWORD d3d::FtoDw(float f) { return *((DWORD*)&f); }

D3DRS_POINTSIZE_MIN — Задает минимальный размер точечного спрайта. Приведенный ниже пример устанавливает минимальный размер равным 0.2:

_device->SetRenderState(D3DRS_POINTSIZE_MIN, d3d::FtoDw(0.2f));

D3DRS_POINTSIZE_MAX — Задает максимальный размер точечного спрайта. Приведенный ниже пример устанавливает максимальный размер равным 5.0:

_device->SetRenderState(D3DRS_POINTSIZE_MAX, d3d::FtoDw(5.0f));

D3DRS_POINTSCALE_A, D3DRS_POINTSCALE_B, D3DRS_POINTSCALE_C — Эти три константы позволяют управлять тем, как будет меняться размер точечного спрайта при изменении расстояния от него до камеры.

Для вычисления итогового размера точечного спрайта на основании расстояния до камеры и рассматриваемых констант Direct3D использует следующую формулу:


Рисование системы частиц



14.2.1. Рисование системы частиц

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

Создать буфер вершин, размер которого достаточен для хранения всех находящихся в кадре частиц.

В каждом кадре:

Обновить данные всех частиц.

Скопировать все живые частицы в буфер вершин.

Нарисовать содержимое буфера вершин.

Этот подход работает, но неэффективен. Во-первых, для того чтобы хранить все частицы системы, буфер вершин должен быть очень большим. Что еще более важно, в момент копирования данных частиц из списка в буфер вершин (этап В) видеокарта будет простаивать. Предположим, что в нашей системе 10 000 частиц; для начала нам понадобится буфер вершин, в котором могут храниться данные 10 000 частиц, а он займет очень много памяти. Помимо этого, видеокарта будет ждать и ничего не делать пока мы не скопируем данные всех 10 000 частиц из списка в буфер вершин и не вызовем метод DrawPrimitive. Этот сценарий хороший пример того, как центральный процессор и видеокарта не работают сообща.

Гораздо лучший подход (который используется в примере Point Sprite из SDK) заключается в следующем:

Создайте буфер вершин большого размера (скажем, такой, в котором может храниться 2000 частиц). Затем разделите буфер на сегменты; в нашем примере размер сегмента будет равен 500 частицам.