Моделирование поверхности воды в режиме реального времени на платформах с многоядерной архитектурой

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

Введение

Специалисты по компьютерному дизайну имеют большой опыт в моделировании объектов реального мира, и их главной задачей является создание виртуальных сред, которые будут выглядеть максимально достоверно. В настоящей статье мы рассмотрим основы моделирования с точки зрения физиков и специалистов по вычислительным системам, работающих в области прикладных наук. В частности, мы рассмотрим моделирование морских волн. Для увеличения производительности решения мы использовали многопоточные методы, использующие преимущества двухпроцессорных платформ. Демонстрация метода, рассмотренного в статье, выполняется в режиме реального времени на системе с двумя процессорами, в том числе и на платформах со встроенными графическими адаптерами, например, на базе чипсетов Intel® 965 Express и чипсетов семейства Intel® 965 Express для мобильных ПК.

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

Предыдущие работы по вопросам рендеринга

Моделированием водной поверхности занималось достаточное количество разработчиков, наиболее успешным среди которых был Тессендорф (Tessendorf). Разработанную им модель океана вы можете встретить в таких фильмах, как «Титаник» (Titanic*) и «Водный мир» (Waterworld*) [5], [6]. В последние годы ряд других разработчиков решали схожие задачи, с учетом возможностей моделирования в режиме реального времени. Например, Мигель Гомес (Miguel Gomez), в своей книге «Интерактивное моделирование водных поверхностей» (Interactive Simulation of Water Surfaces) [2] описал решение для вычисления полей высот. В некоторых случаях такое решение можно считать оптимальным, однако его главным недостатком является необходимость сохранения в памяти по меньшей мере двух сеток рендеринга – вычисленной ранее и текущей, чтобы рассчитать новую. Еще один минус этого решения – необходимость получения данных о соседних вершинах перед вычислением новой позиции каждой из них. Прямое же решение, не требующее таких данных, было описано Марком Финчем (Mark Finch) в книге «Эффективное моделирование водных поверхностей на основе физических моделей» (Effective Water Simulation from Physical Models) [3]. Оно обладает целым рядом преимуществ:

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

Джон Айсидоро (John Isidoro) в книге «Рендеринг океанской воды» (Rendering Ocean Water) [4] описал метод суммирования синусоидальных волн, схожий с рассмотренным в книге Марка Финча [3]. В этих работах представлен программный код, реализующий вершинный и пиксельный шейдеры DirectX8 и обзор реализации используемого метода.. Алгоритм, представленный в настоящей статье, основан на работах [3] и [4] и использует вычислительные возможности центрального процессора , но может быть легко перенесен на HLSL (High Level Shader Language).

Теоретические сведения

Рассмотрим несколько основных понятий волновой теории [Giancoli85], [3].

Амплитуда волны (amplitude): величина пика или впадины относительно положения равновесия. Сумма абсолютных величин пика и впадины волны называется двойной амплитудой (размахом).

Длина волны (wavelength): расстояние между двумя ближайшими гребнями волны.

Скорость волны (velocity): определяет перемещение волны в среде с течением времени. φ, фазовая постоянная, характеризует распространение волны в среде и определяется по формуле φ = скорость волны * (2 π)/длина волны

3.1 Метод суммирования гармонических колебаний для создания волн

Рис. 3-1. Физические свойства волны

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

3.2 Моделирование статических волн

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

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

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

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

Для реализации следующей цели – управления высотой волн (амплитудой) – добавим в формулу масштабный коэффициент:

3.3 Моделирование динамических волн

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

Поскольку мы моделируем двухмерное поле высот, нужно предусмотреть движение волн в обоих направлениях. Для этого необходимо вычислить проекцию заданных координат х и у на вектор направления волны путем скалярного произведения. Предположим, что вектор направления волны параллелен плоской поверхности и не имеет координаты z (это упростит вычисления). Результатом скалярного произведения двух векторов является скалярная величина, которую мы обозначим как S:

Теперь предусмотрим возможность изменения частоты волны. Из курса физики известно соотношение, описывающее связь длины волны с ее частотой: частота = 2 pi / длина волны. Поэтому воспользуемся длиной волны в качестве исходных данных для вычисления частоты по формуле, описанной выше. Добавим в формулу S новый множитель – частоту, чтобы иметь возможность изменять частоту нашей синусоидальной функции:

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

Преобразуем нашу формулу следующим образом (t – время):

В итоге мы получили функцию, в которой учтены длина, амплитуда, скорость, направление, положение и крутизна волны:


Где


и

3.4 Построение волн

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

3.5 Нормали к поверхности

Для затенения поверхности необходимо вычислить нормаль к ней. Предположим, мы используем мозаичную поверхность. Тогда по формуле 3 вычисляются новые координаты вершин, после чего вычисляются нормали ребер, а затем усредняются для каждой вершины – это позволит найти нормали к поверхности. Существует и альтернативный подход – прямое решение, описанное в книге [3]. Для определения скорости изменения нормали к поверхности возьмем производные в направлениях х и у (назовем эти производные как вектор бинормали и вектор тангента соответственно). Выше было отмечено, что любая точка (x,y) имеет некую высоту поверхности, определяемую функцией, то есть точка(x,y,t) = (x,y,f(x,y,t)). Тогда вектор бинормали и вектор тангента для любого поля высот будут определяться следующими выражениями (для упрощения выкладок предположим, что поле высот ориентировано параллельно координатной сетке х-у):


После упрощения формула принимает вид


и


После упрощения формула принимает вид

Произведение векторов определяется формулой:

Вычислим производные функции f(x,y,t) для каждой синусоидальной волны и просуммируем их. Для этого необходимо взять частные производные функции f(x,y,t) по х и у для каждой волны, участвующей в моделировании поверхности:

Где

Частная производная по у вычисляется аналогично:

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

3.6 Многопоточность

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

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

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

Рис. 3-2. Моделирование с использованием двух потоков

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

Поток А является основным и управляет инициализацией, искусственным интеллектом, взаимодействием с пользователем, рендерингом и последовательностью завершения приложения; поток В осуществляет моделирование, причем в этот момент поток А простаивает, потому что моделируется только один объект - вода. Таким образом, наша задача сводится к распределению нагрузки между потоками А и В так, чтобы ни один поток не ждал завершения другого, или время ожидания было сведено к минимуму. Правильное распределение нагрузки является важным требованием при увеличении количества потоков, поскольку обеспечивает максимальный выигрыш от многопоточности.

Реализация

Наша первая программная реализация основывалась на методах, описанных в разделе [2]. Напомним, что необходимость получения данных по соседним вершинам не позволяет в данном методе использовать многопоточность [1]. Еще одним недостатком является отсутствие параметров управления видом водной поверхности. Поскольку мы хотели создать волны, непрерывно бегущие по поверхности моря, мы решили рассмотреть воду подобно эластичной мембране, которая, будучи натянутой и отпущенной, будет колебаться. Соответственно, мы выбрали метод, описанный в статье [3] (его преимущества были рассмотрены в Главе 2).

Рис. 4-1. Моделирование морских волн

Результат программной реализации продемонстрирован на рис. 4-1. В левом верхнем углу окна расположены параметры для каждой синусоидальной волны, влияющие на свойства моделируемой поверхности. В правом верхнем углу – кнопки для включения и отключения многопоточного режима, выбора метода вычисления нормалей и сохранения параметров для последующего использования. Наша демонстрационная программа является доработанной версией программы BasicHLSL, рассмотренной в статье [11].

4.1 Интерфейс пользователя

Одним из преимуществ метода, описанного в статье [3], является возможность полного контроля параметров моделируемой поверхности. В нашей программе такой контроль осуществляется в режиме реального времени. При этом можно изменять амплитуду, скорость, направление и крутизну волны (см. формулу 2). Более того, эти параметры можно сохранять и вызвать при последующем моделировании. Кнопка включения и отключения многопоточного режима, расположенная в правой части окна интерфейса, используется для сравнения производительности. Параметры конфигурации волн можно скрыть, чтобы увеличить масштаб изображения.

4.2 Реализация суммирования гармонических колебаний с параметром крутизны на языке C++

Рассмотрим программную реализацию многопоточного метода, использующего программное суммирование синусоидальных функций (программа составлена с учетом формулы 2):

void CSinWaterMesh::TakeStepSumOfWavesWithExp( float t,

int numOfWavesToSum )

{

for( int i=0; i<m_iNumRows; i++ )

{

for( int j=0; j<m_iNumCols; j++ )

{

for( int k=0; k<numOfWavesToSum; k++ )

{

CVector3 posVect;

float dotresult = 0.0f;

float phase_constant = 0.0f;

float final = 0.0f;

posVect.Init( m_pVB[i*m_iNumCols+j].x,

m_pVB[i*m_iNumCols+j].y,

0.0f );

if( m_bSumWave[k] )

{

dotresult = m_direction[k].Dot( &posVect );

dotresult *= ( 2*(float)MYPI ) / m_wavelength[k];

phase_constant = t*

( (m_speed[k]*2*(float)MYPI) / m_wavelength[k] );

final = ( dotresult + phase_constant );

final = ( sin(final) + 1.0f ) / 2.0f;

final = m_amplitude[k] * pow( final, m_kexp[k] );

}

else

{

final = 0.0f;

}

 

if( k!=0 )

{

m_pVB[i*m_iNumCols+j].z += final;

}

else

{

// The first wave calculated will overwrite the

// summation from the last frame.

m_pVB[i*m_iNumCols+j].z = final;

}

}

}

}

}

4.3 Вычисление нормалей

Вначале мы использовали прямой метод вычисления нормалей, описанный в Формуле 4. Мы ожидали получить хорошую производительность, но оказалось, что значительно быстрее вычислять нормали к вершинам путем усреднения нормалей ребер. Ускорение происходит потому, что у нас нет необходимости осуществлять трудоемкий поиск соседних вершин по всему кадру - мы заранее вычисляем список соседей для каждой из вершин. Кстати, это невозможно осуществить в программе, использующей GPU и DirectX9, т.к. DirectX9 не предоставляет доступ к таким данным. Таким образом, формула 4 будет оптимальной для вычисления нормалей с использованием GPU+DirectX. Методика усреднения нормалей дает хорошие результаты для вычислений с помощью CPU, даже с учетом вычислений новых нормалей граней.

4.4 Многопоточность

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

Во второй реализации вместо запуска потоков по запросу использовался пул потоков. В нашем случае в пул помещается единственный вспомогательный поток, который взаимодействует с основным потоком, выполняющим рендеринг. Работа с пулом потоков позволяет избежать издержек, возникающих при каждом запуске и завершении потока: все потоки создаются при старте программы, и используются тогда, когда это необходимо основному потоку. У такого метода есть и свои недостатки: ресурсы выделяются даже для тех потоков, которые в данный момент времени не используются. Кроме того, в зависимости от способа реализации пула, могут существовать потоки, занимающие процессорное время выполнением циклов ожидания. Для предотвращения холостой работы процессора обычно используется метод с периодическим опросом или объект синхронизации операционной системы. Наша идея заключалась в том, чтобы уменьшить издержки, связанный со дополнительными потоками и создать только один дополнительный поток, перенеся туда часть работы основного потока рендеринга. Мы решили имитировать дополнительную нагрузку игрового движка в потоке А, пока поток В вычисляет сетки и нормали. Эта дополнительная нагрузка может занимать некоторое процессорное время, но ее результат не влияет на вид водной поверхности, - например, в реальной игре дополнительной нагрузкой может стать расчет физических аспектов сцены, искусственный интеллект, взаимодействие с пользователем и т.п. Мы рекомендуем в таких случаях распараллеливать приложение на уровне задач: например, в нашем случае, вычислять поверхность воды в одном потоке и генерировать нормали в другом. Альтернативным вариантом может стать декомпозиция на уровне циклов, при которой разные сегменты сетки вычисляются разными потоками. Однако, этот метод может давать некоторые издержки в процессе соединения сегментов сетки, если для этого требуется информация о соседних вершинах.

Создание потока

Для создания потоков мы используем функцию __beginthreadex(…), выбор которой объясняется результатом сравнения win32-функций и run-time библиотек языка С, рассмотренных в статье [1]. Нам показалось, что функция __beginthreadex(..) более надежна и удобна в обращении по сравнению с аналогичной функцией CreateThread(..). Это подтвердилось и в ходе наших изысканий. Согласно документации по Microsoft Visual Studio.Net 2005*, при использовании функции CreateThread(…) во время выполнения программы на языке C может возникнуть «утечка памяти», когда поток вызывает функцию ExitThread(…).

#include <process.h>

HANDLE hThreadHandle; //unsigned long

DWORD dwThreadid;

.

.

hThreadHandle = (HANDLE) _beginthreadex(void *security,

unsigned stack_size,

unsigned (_stdcall *)(void *),

void *arg,

unsigned initflag,

unsigned *threadaddr,

);

Вызов функции __beginthreadex(..) имеет несколько параметров. Первый параметр содержит структуру атрибутов безопасности. Когда значение этого параметра установлено в NULL, поток получает уровень безопасности по умолчанию. Второй параметр определяет размер стека. Если его значение равно 0, размер стека будет как у текущего потока. Третий параметр – адрес функции пользователя, которая будет вызвана новым потоком в начале его выполнения. Оставшиеся три параметра: arg – значение, передаваемое новому потоку, initflag – дополнительный флаг для управления состоянием потока при его запуске, threadaddr – адрес области памяти, в которую будет записан идентификатор потока. В нашем приложении вызов описан следующим кодом:

hThreadHandle = (HANDLE) _beginthreadex( NULL,

0,

LaunchTakeStepThread,

(void*)&g_time,

0,

(unsigned int*)&dwThreadId );

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

Выполнение потоков

Запущенный поток находится в состоянии ожидания, выполняя функцию sleep() до тех пор, пока не получит команду на выполнение полезной работы. Функция sleep() принимает интервал ожидания в миллисекундах. Если переданный ей аргумент равен 0, функция прерывает текущий интервал ожидания и ждет следующего вызова. Когда мы готовы начать выполнение второго потока, основной поток сообщает об этом вспомогательному, увеличив значение общей переменной, находящейся в области памяти, доступной обоим потокам. Основной поток увеличивает значение общей переменной, чтобы сообщить вспомогательному потоку о завершении обработки предыдущей порции данных и готовности принять новую сетку. После этого вспомогательный поток вычисляет новый набор данных, передает их и переходит в sleep до тех пор, пока его не «разбудит» основной поток. Такая модель программирования называется «продавец/покупатель».

Рассмотрим программный код «продавца» (вспомогательного потока):

while(we are not exiting the thread)

{

TakeStep();

Time += Time_Increment;

bStepComplete = true;

while(bStepComplete && !bExitThread)

{

Sleep(0);
}

}

The master thread consumes the mesh produced by the helper thread and tells the thread to go ahead and compute the next mesh.

while(1)

{

while(!bStepComplete)

{

// Wait for the grid update to finish

Sleep(0);

}

 

//Copy the vertex info to the “real” VB.

g_pGrid->CopyVBToRenderVB();

 

//Allow the other thread to compute a new set of vertices

g_pGrid->ResetStepComplete(); //resets bStepComplete

}

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

Удаление потока

В модели, описанной выше, вспомогательный поток будет автоматически завершен, когда закончится выполнение функции, вызванной с помощью beginthreadex(…). Иногда для завершения потока может потребоваться функция _endthreadex(..), однако в нашей имплементации этого не потребовалось.

4.5 Взаимные исключения

В разделе «Выполнение потоков» описана использованная нами модель «продавец/покупатель»: «продавцом» является вспомогательный поток, вычисляющий сетку для «покупателя» – основного потока. Классическая модель «продавец/покупатель» подразумевает, что «продавец» помещает подготовленные им данные в область памяти, откуда их достает «покупатель» для последующей обработки. Размер очереди зависит от того, насколько продавец может "опередить" покупателя. В нашем случае синхронизация происходит в каждом кадре, причем поток-«продавец» не переходит к вычислению данных для следующего кадра, пока данные по текущему кадру не были востребованы потоком-«покупателем». Так происходит потому, что во многих играх следующие кадры используют данные текущего кадра (например, при работе искусственного интеллекта, физических вычислениях или обработке команд, поступающих от игрока).

В нашей программе переменная m_bStepDone, по значению которой можно судить, была ли обновлена сетка с момента ее последней обработки, периодически опрашивается обоими потоками. Для защиты этой переменной мы использовали два метода синхронизации, рассмотренных в книге Аарона Коэна «Многопоточное программирование Win32-приложений» (Aaron Cohen,Win32 Multithreaded Programming): доступ со взаимной блокировкой и критические секции [1]. Выбор правильного метода определяется в основном функциональными особенностями. Блокировка – самый простой и лучший способ для защиты доступа к общим переменным. Критические секции более универсальны и подходят для обработки больших объемов данных. В примере, рассмотренном в настоящей статье, допускается использование любого варианта, которые, кстати, считаются самыми производительными примитивами синхронизации, доступными в Windows.

Доступ с блокировкой осуществляется с применением набора функций, напрямую использующих инструкции CPU «чтение-изменение-запись». Эти инструкции позволяют предотвратить ситуации, при которых переменные принимают ошибочное значение из-за конфликтов чтения-записи между несколькими потоками.

Для осуществления блокировки пригодны только три функции:

InterlockedIncrement(), InterlockedDecrement() и InterlockedExchange().

Функции InterlockedIncrement и InterlockedDecrement соответственно увеличивают и уменьшают значение переменной, а функция InterlockedExchange позволяет заменить одно значение переменной на другое с помощью одной элементарной операции.

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

Свойства среды Visual Studio

Рекомендуем уделить особое внимание правильной настройке среды Visual Studio. В частности, выберите флаги компилятора для VC++ с в меню Project->Properties->Configuration Properties->C/C++->Code Generation->Runtime Library и указания нужной многопоточной библиотеки. На рис. 4-2 изображено окно свойств в разделе библиотеки исполняемых компонентов.

Рис. 4-2. Окна свойств в разделе библиотеки исполняемых компонентов.

Производительность

5.1 Оптимизация программного кода

Для анализа производительности программного кода мы воспользовались анализатором производительности Intel® VTune™. Оказалось, что большую часть времени выполнения вспомогательный поток обрабатывал функцию LookupTriIndex() для шестикратного вычисления индексов треугольников для каждой вершины, после чего выбирал нормали треугольников, подходящие для усреднения, и вычислял новую нормаль вершины. Поскольку LookupTriIndex() – линейная функция, вычисление нормалей имеет порядок выполнения O(n2). Процесс вычисления нормалей повторялся несколько раз. У нас было два варианта: ускорить процесс вычисления нормалей или вычислить эти нормали непосредственно по частным производным уравнений гармонических колебаний. Для сравнения мы реализовали оба этих варианта, а также сохранили исходный алгоритм.

Рис. 5-1. Сравнение производительности в системах с одним и двумя процессорами.

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

5.2 Анализ производительности

Анализ производительности проводится по нескольким параметрам. Для проверки нашего приложения мы создали 3 набора данных, имитирующие 3 различные сценария. Данные были подобраны таким образом, чтобы увеличить нагрузку на первый поток, перед запросом новой порции данных у потока 2. Однопроцессорная система показывала меньшую производительность при увеличении нагрузки, что ожидаемо: один процессор должен выполнить бОльшую работу. Обратите внимание, что в двухпроцессорной конфигурации разница в FPS невелика в случаях сценариев 1 и 2. Иными словами, система с двумя процессорами оказалась хорошо сбалансированной для обработки второго набора данных. Однако, третий набор данных вызвал существенное падение FPS для всех трех сеток, даже на двухпроцессорной машине. Это означает, что нагрузка основного потока превысила нагрузку вспомогательного, который вычислял сетки и нормали. Таким образом, производительность кода стала зависеть именно от основной загрузки, а не от геометрических вычислений.

5.3 Распределение нагрузки

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

Характерно, что абсолютная производительность уменьшается с увеличением относительной. Это видно из диаграммы в случае третьего набора данных и сеток большого размера. Следовательно, для больших сеток сложные данные обеспечивают более равномерное распределение нагрузки. Возможно, лучшее распределение будет где-то в промежутке между сценариями 2 и 3 для больших сеток, и где-то между сценариями 1 и 2 – для сетки размером 40х40. Подытожим: производительность многопоточного приложения будет максимальной, когда каждый поток имеет нагрузку, значительно превышающую системные издержки, и все нагрузки сбалансированы по отношению друг к другу.

Перспективы

Итак, мы закончили работу по моделированию морских волн. Но для максимально достоверного моделирования осталось решить несколько дополнительных задач. Во-первых, можно реализовать вычисление карт нормалей аналогично тому, как это делалось для моделирования морских волн. Это усовершенствует нашу модель и лучше отразит компоненты волн, имеющие повышенные частоты (например, рябь от ветра). Форма получится не вполне достоверная, но она будет обладать всеми преимуществами и недостатками карт нормалей в других ситуациях. Есть над чем поработать и в области освещения и затенения моделируемой поверхности. Наиболее важными вопросами в данном случае является вычисление освещения с учетом векторов отражения и преломления. Мы также планируем реализовать некоторые методы, описанные в книге [8], чтобы усовершенствовать освещение с использованием технологий изображений с расширенным динамическим диапазоном (HDR). Мы планируем поработать и над моделированием «барашков» на поверхности волн.

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

Об авторах

Адам Лэйк (Adam Lake) – главный разработчик программного обеспечения группы программного обеспечения и решений корпорации Intel, возглавляющий проект «Современные игровые технологии». Его специализация – архитектуры и алгоритмы компьютерной графики нового поколения. Подробнее

Дэвид Регин (David Reagin) – разработчик ПО корпорации Intel. В течение 7 лет он занимался проверкой схем микропроцессоров, после чего увлекся разработкой графических приложений. Он получил степень бакалавра вычислительной техники в Технологическом институте Джорджии.

Справочные материалы

1. [Cohen98] Аарон Коэн (Aaron Cohen) и Майк Вудринг (Mike Woodring). «Многопоточное программирование Win32-приложений» (Win32 Multithreaded Programming). Издательство O’Reilly and Associates. 1998.

2. [Gomez00] Мигель Гомес (Miguel Gomez). «Интерактивное моделирование водных поверхностей» (Interactive Simulation of Water Surfaces). «Сокровищница игрового программирования. Часть 1» (Game Programming Gems 1). Под редакцией Марка Делора (Mark Deloura). Стр. 187-194.

3. [Finch04] Марк Финч (Mark Finch). Эффективное моделирование водных поверхностей из физических моделей» (Effective Water Simulation from Physical Models). «Секреты компьютерной графики: методы программирования, советы и приемы работы для дизайна в режиме реального времени» (Programming Techniques, Tips, and Tricks for Real-Time Graphics). Под редакцией Рандима Фернандо (Randima Fernando). Стр. 5-29. 2004 г.

4. [Isidoro02] Джон Айсидоро (John Isidoro), Алекс Влачос (Alex Vlachos) и Крис Бреннан (Chris Brennan). «Рендеринг океанской поверхности» (Rendering Ocean Water). «Direct3D ShaderX: Советы и хитрости при работе с вершинными и пиксельными шейдерами» (Direct3D ShaderX: Vertex and Pixel Shader Tips and Tricks). Стр. 347-356. 2002 г.

5. [Tessendorf01] «Моделирование океанской поверхности» (Simulating Ocean Water). SIGGRAPH2001 Course Notes. 2001.

6. [IMDB04] Титры к фильму «Титаник».http://us.imdb.com/title/tt0120338/.

7. [Vterrain04] Web-сайт http://www.vterrain.org/Water/.Ссылки по теме «Моделирование водной поверхности». 2004.

8. [Lake04] Адам Лэйк (Adam Lake) и Коди Нортроп (Cody Northrop) «Высококачественное наложение структур с расширенным динамическим диапазоном в режиме реального времени» (Real-Time High Dynanmic Range Environment Mapping). Готовится к публикации.

9. [QuickMath04]http://www.quickmath.com/. 4 ноября 2004 г.

10. [Giancoli85] Дуглас Джанколи (Douglas Giancoli). «Физика. Теория и практическое применение» (Physics, Principles with Application), 2-е издание. Prentice Hall, Inc. 1985 г.

11. [MSSDK04] Обновление комплекта для разработчика Microsoft Corporation DirectX 9.0 SDK (версия, вышедшая летом 2004 г.). http://www.microsoft.com/downloads/search.aspx?displaylang=en&categoryid=2. Август, 2004 г.

Дополнительные изображения

WaveDemo code sample (ZIP 2.9 MB)

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