netlib.narod.ru | < Назад | Оглавление | Далее > |
Системой частиц называется набор частиц и код, отвечающий за управление этими частицами и их отображение. Система частиц отслеживает глобальные свойства, влияющие на все частицы, такие как размер частиц, место из которого появляются частицы, накладываемая на частицы текстура и т.д. С точки зрения функциональности система частиц отвечает за обновление, отображение, уничнтожение и создание частиц.
Хотя различные системы частиц ведут себя по-разному, мы можем выполнить обобщение и выделить некоторые базовые свойства, которые используются всеми системами частиц. Мы поместим эти общие свойства в абстрактный базовый класс 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 — Количество частиц, которое может быть помещено в буфер вершин одновременно. Это значение не зависит от количества частиц в конкретной системе частиц.
Методы класса:
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++; // следующий элемент списка } } }
Поскольку система частиц является динамической, нам надо обновлять данные частиц в каждом кадре. Первый приходящий на ум, но неэффективный метод визуализации системы частиц заключается в следующем:
Создать буфер вершин, размер которого достаточен для хранения всех находящихся в кадре частиц.
В каждом кадре:
Обновить данные всех частиц.
Скопировать все живые частицы в буфер вершин.
Нарисовать содержимое буфера вершин.
Этот подход работает, но неэффективен. Во-первых, для того чтобы хранить все частицы системы, буфер вершин должен быть очень большим. Что еще более важно, в момент копирования данных частиц из списка в буфер вершин (этап В) видеокарта будет простаивать. Предположим, что в нашей системе 10 000 частиц; для начала нам понадобится буфер вершин, в котором могут храниться данные 10 000 частиц, а он займет очень много памяти. Помимо этого, видеокарта будет ждать и ничего не делать пока мы не скопируем данные всех 10 000 частиц из списка в буфер вершин и не вызовем метод DrawPrimitive. Этот сценарий хороший пример того, как центральный процессор и видеокарта не работают сообща.
Гораздо лучший подход (который используется в примере Point Sprite из SDK) заключается в следующем:
Создайте буфер вершин большого размера (скажем, такой, в котором может храниться 2000 частиц). Затем разделите буфер на сегменты; в нашем примере размер сегмента будет равен 500 частицам.
Рис. 14.1. Разделенный на сегменты буфер вершин
Теперь создайте глобальную переменную i = 0 для отслеживания того, с каким сегментом мы работаем.
Для каждого кадра:
Обновите все частицы.
Пока не будут визуализированы все живые частицы:
Если буфер вершин не заполнен, то:
Блокируем сегмент i с флагом D3DLOCK_NOOVERWRITE.
Копируем 500 частиц в сегмент i.
Если буфер вершин заполнен, то:
Возвращаемся к началу буфера вершин: i = 0.
Блокируем сегмент i с флагом D3DLOCK_DISCARD.
Копируем 500 частиц в сегмент i.
Визуализируем сегмент i.
Переходим к следующему сегменту: i++
Данный подход более эффективен. Во-первых, мы сокращаем размер необходимого нам буфера вершин. Во-вторых, теперь центральный процессор и видеокарта работают в унисон; то есть мы копируем небольшую партию частиц в буфер вершин (работа центрального процессора), а затем мы визуализируем эту партию частиц (работа видеокарты). Затем мы копируем в буфер вершин следующую партию частиц и рисуем ее. Это продолжается до тех пор, пока не будут визуализированы все частицы. Как видите, видеокарта больше не простаивает, ожидая пока не будет заполнен весь буфер вершин.
Теперь мы обратим наше внимание на реализацию этой схемы визуализации. Чтобы облегчить визуализацию системы частиц с помощью данной схемы мы будем использовать следующие члены данных класса 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()
В системах частиц есть своего рода хаотичность. Например, моделируя снегопад мы не хотим, чтобы все снежинки падали абсолютно одинаково. Нам нужно чтобы они падали похожим образом, а не абсолютно одинаково. Чтобы облегчить реализацию хаотичности, необходимую для систем частиц, мы добавляем в файлы 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); }
netlib.narod.ru | < Назад | Оглавление | Далее > |