Задание 2: Процедурный морфинг поверхности на GPU

Авторы: Фролов В., Тисевич И.

Начало: 23.04.2012
Конец: 15.05.2012

Описание задания

Примечание 1: для выполнения задания потребуется разработка программы на машине с видеокартой, поддерживающей OpenGL 3.2 или выше.

Цель задания – изучение общих принципов аппаратно ускоренной визуализации и API OpenGL 3.0, получение навыков написания шейдерных программ, корректной работы с топологией трёхмерных моделей и применения ээфектов пост-процессинга и обработки изображений на GPU.

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

Рисунок 1. пример реализации задания. Применён эффект пост-процессинга “свечение” (bloom).

Обязательная часть

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

К модели при помощи шейдеров (вершинных и/или геометрических) должно применяться нетривиальное преобразование изменения формы, плавно изменяющее её внешний вид с течением времени. Скорость изменения формы поверхности должна зависеть только от времени и не должна зависеть от частоты кадров (FPS). Преобразование не должно нарушать топологию модели (в ней не должно быть щелей, явных самопересечений, должна сохраняться гладкость поверхности за исключением, может быть, ограниченного числа точек), преобразование должно сохранять корректное поле нормалей модели с учётом изменённой формы поверхности. В базовом варианте разрешается рассчитывать нормали для освещения типа flat shading, при котором каждый примитив модели имеет свою постоянную нормаль на всей площади, то есть поле нормалей не является гладким. Необходимо реализовать как минимум два разных преобразования. Также модель должна изменять цвет по некоторому закону во времени, этот эффект можно реализовать с помощью шейдера любого типа. Более сложные и интересные реализации получат дополнительные баллы.

Дополнительная часть

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

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

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

Предлагается большой набор эффектов пост-процессинга, за реализацию которых также даются дополнительные баллы. Эффекты должны быть реализованы с помощью шейдерных программ. Для реализации эффектов разрешается использовать любое количество проходов и дополнительных текстур (буферов) до тех пор, пока они занимают разумный объём видеопамяти. Многие из этих эффектов вы знаете по лекциям, посвящённым обработке изображений и компьютерному зрению. Интересные эффекты, не входящие в список, также будут оценены, но про них нужно явно упомянуть в readme-файле.

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

Оценка

База (16):

  • Изменение формы поверхности и ее цвета нетривиальным образом.
  • Корректное освещение:
    • Расчет нормалей в геометрическом или вершинном шейдере. Минимум необходимо рассчитывать нормаль для треугольника - “flat shading”.
    • Два или более источника света.
  • Показ wireframe модели одновременно (как в примере) или вместо основной модели.
  • Вращение модели или камеры (хотя бы автоматическое).
  • Если реализовано несколько типов морфинга, переключение типа морфинга должно осуществляться клавишами 7-9.
  • Включение/отключение каркасной модели (wire frame) клавишей 6.
  • Эффекты пост обработки должны включаться по клавишам 1-5
  • Клавиша 0 должна отключать все эффекты пост-обработки.

Вы также можете реализовать GUI. В этом случае требование по клавишам описанное выше не обязательно.

Дополнительная часть:

  • Текстурирование моделей (+2)
  • Реалистичное освещение
    • ДФО Кука-Торранса (+2)
    • Анизотропная модель ДФО (+4)
  • Окружение (плоскость, любые трёхмерные модели, скайбокс) (+2)
  • Отражения на поверхности (с помощью кубических карт) (+2-4)
    • Можно просто один раз загрузить панораму и не обновлять кубическую карту (+2)
    • Рендеринг в кубическую карту текущего окружения (+4)
  • Проективные тени на плоскость (+2)
  • Самозатетение – Shadow Volumes или Shadow Maps (+6-8) (обязательна реализация полностью на GPU)
  • Красивый морфинг (+2)
  • Использование модели, отличной от сферы, данной в примере (+2)
  • Красивые переливающиеся цвета (+2)
  • Сглаженные нормали (smooth shading) (+6)
  • Использование аппаратной тесселяции (+6-8). Уровень тесселяции должен быть адаптивным (по краям или по расстоянию) или контролироваться извне посредством нажатия клавиш или GUI.
  • Эффекты пост-обработки:
    • Простое размытие (+1)
    • Гауссово, сепарабельное размытие (+2)
    • Простое увеличение резкости (+1)
    • Дизеринг (dithering) (+2)
    • Размытие по глубине (Depth Of Field) (+2-6)
    • Bloom (+2-6)
    • Тиснение (+2)
    • Выделение границ (+2)
    • Motion Blur (+2-4)
    • Screen Space Ambient Occlusion (+2-6)

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

Пример реализации задания: http://www.youtube.com/watch?v=5eDuHwK8DLQ

Данное задание получило бы 12 баллов за базу, 6 балла за Bloom и по 2 балла за красивый морфинг и переливающиеся цвета – итого 22 балла.

Рисунок 2. Пример реализации задания.

В данном задании вам будет предоставлен простой пример работы с вершинными, пиксельными и геометрическими шейдерами (которые работают на Intel HD3000). В примере визуализируется сфера с простым мофингом и зеленым цветом.

Теоретические основы

Графический конвейер DirectX 7 (OpenGL 1.0)

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

Рисунок 3. Фиксированный графический конвейер OpenGL 1.0.

Фиксированный конвейер имеет свои преимущества (например простота), но имеет и очевидные недостатки.

Графический конвейер DirectX 9 (OpenGL 2.0 и OpenGL 3.0)

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

В DirectX9 и OpenGL 2.0/3.0 вам доступны 2 типа программ – вершинные и фрагментные. Вершинные программы принимают на вход одну вершину, обрабатывают ее неким образом и отправляют дальше по конвейеру. На выходе помимо позиции для каждой вершины могут сохранятся какие-то еще атрибуты (вообще говоря, числа произвольного назначения) - нормали, текстурные координаты, какой-то специальный цвет. Все атрибуты, поступившие на выход вершинной программы, будут линейно интерполироваться внутри треугольника.

Рисунок 4. DirectX 9 pipeline.

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

Другой ситуацией, когда “пикесел != фрагмент”, является включенное альфа-смешивание (которое осталось таким же как и в OpenGL 1.0). Если оно включено, то пикселы получатся путем смешения фрагментов, и реально в один пиксел может попадать несколько фрагментов.

Графический конвейер DirectX 10 (OpenGL 3.2)

В графическом конвейере DirectX 10 появилась возможность управлять не только отдельными вершинами, но и целыми примитивами. Геометрический шейдер принимает на вход примитив (точку, треугольник или треугольник с некоторым его окружением из других треугольников) и на выходе генерирует другие примитивы – triangle strip или line strip – последовательность связанных треугольников или линий. В дальнейшем вы увидите, что возможность работать со всеми 3 вершинами треугольника вам действительно иногда нужна.

Рисунок 5. DirectX 10 Pipeline.

Новые API графики, такие как DirectX 10 и 11, и OpenGL 3 и 4 пошли по пути облегчения предоставляемой функциональности, поэтому если вам кажется, что OpenGL 3 – это тонкая прослойка над драйвером, то, в принципе, так и есть.

Описание алгоритма и расширение библиотеки glHelper

Первое, что нужно сделать – описать функцию морфинга “float3 Morph(time,position)”. Эта функция будет принимать на вход позицию вершины и время, а возвращать новую позицию вершины. Изменяя позицию в вершинном шейдере мы получим морфящуюся поверхность.

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

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

Рендеринг Меша (SimpleMesh)

Отправлять на отрисовку геометрию в OpenGL 3 стало немного сложнее.

Объект “Buffer” представляет просто область памяти на GPU. Вовсе не обязательно использовать его для хранения вершин. Но если там лежат вершины, то такой буфер называют VBO (Vertex Buffer Object). Для того чтобы отрендерить геометрию, ее нужно как минимум загрузить в память GPU. Этим и занимается следующий код в конструкторе класса SimpleMesh:

glGenBuffers(1, &m_vertexPosBufferObject);                                                   
glBindBuffer(GL_ARRAY_BUFFER, m_vertexPosBufferObject);                                      
glBufferData(GL_ARRAY_BUFFER, m_glusShape.numberVertices * 4 * sizeof(GLfloat), (GLfloat*) m_glusShape.vertices, GL_STATIC_DRAW);

Рисунок6. Концепция Vertex Array Object.

Затем создается объект VAO (Vertex Array Object), который будет говорить, что где лежит. Например, вот так мы указываем, что позиции (атрибут с именем “vertex”) будет находиться в буфере m_vertexPosBufferObject.

m_vertexPosLocation = glGetAttribLocation(a_programId, "vertex");
if(m_vertexPosLocation < maxVertexAttributes)
{
  glBindBuffer(GL_ARRAY_BUFFER, m_vertexPosBufferObject);                    
  glEnableVertexAttribArray(m_vertexPosLocation);                            
  glVertexAttribPointer(m_vertexPosLocation, 4, GL_FLOAT, GL_FALSE, 0, 0);   
}

Это похоже на функциональность OpenGL 1.0

glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(vertexChannels, GL_FLOAT, size, pointerToVertexPos);

Но теперь все данные уже находятся на GPU в буфере m_vertexPosBufferObject.

В предоставляемом вам примере все данные вершин (позиция, текстурные координаты, нормаль) лежат в трёх разных буферах.

Класс SimpleMesh, которым вы можете пользоваться, абстрагирует вас от низкоуровневых деталей OpenGL и таких понятий как VBO и VAO. Однако, если вы захотите сделать, например, вычисление гладких нормалей или еще что-то специальное, вам скорее всего придется сделать свою реализацию класса для мешей.

Экранные эффекты

Для реализации экранных эффектов используется знакомая вам техника растеризации полноэкранного прямоугольника. Вы можете начать изучение экранных эффектов с урока 7 [2]. В каждом пикселе экрана выполняется фрагментный шейдер, который записывает результат в другую текстуру. Для того чтобы вместо вывода на экран результат пиксельного шейдера записывался в текстуру, вам необходимо привязать текущий Frame Buffer Object, который и укажет, в какую текстуру нужно рендерить.

Мы предоставляем вам класс RenderableTexture2D, который упростит процесс рендеринга в текстуру (но, разумеется, его использовать не обязательно). Использовать этот класс нужно так:

g_pMyTexture->BeginRenderingToThisTexture();
 
glUseProgram(g_myProgram.program); 
// тут выставляем юниформы и привязываем текстуры
//
g_pFullScreenQuad->Draw();
 
g_pMyTexture->EndRenderingToThisTexture();
g_pMyTexture->BuildMipMapChain();

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

Моделирование отражений

Для моделирования отражений необходимо сделать 2 вещи:

  1. Привязать к шейдерной программе кубическую текстуру myCubeTex
  2. Вычислить отраженный вектор r
  3. Сделать выборку из кубической текстуры вектором r

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

Рисунок 7. Алгоритм вычисления отражений при помощи кубических текстурных карт.

Рисунок 8. Отражения, смоделированные при помощи кубических текстурных карт. DirectX 10 SDK .

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

Подсказки

Wire Frame

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

Гладкие нормали

Для того чтобы рассчитать гладкие нормали необходимо:

  1. Использовать входной тип примитива для геометрического шейдера - 'triangles_adjacency'.
  2. Использовать вызов
    glDrawElements(GL_TRIANGLES_ADJACENCY,6*mesh.numTris,GL_UNSIGNED_INT, NULL);
    вместо
    glDrawElements(GL_TRIANGLES,3* mesh.numTris, GL_UNSIGNED_INT, 0);
  3. Построить свой собственный меш сферы, а не использовать glusShape. И для этого меша вычислить индексы так как это делается в [1] функцией makeAdjacencyIndex.
  4. В геометрическом шейдере обойти все смежные треугольники, посчитать нормали для них и взять нормаль в вершине как среднее (или взвешенное среднее). В качестве весов могут быть использованы углы между смежными с данной вершинами ребрами для данного треугольника.

Инвертирование нормали

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

float3 v = normalize(g_camPos - fragmentWorldPos);
float3 n = fragmentNormal;
 
if(dot(v,n) <= 0.0f)
  n *= (-1.0);

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

Привязка текстур

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

Правильно:

glUseProgram(program); 
TextureSetup(program, 1, "colorTexture", texId1);
TextureSetup(program, 2, "otherTexture", texId2);

Неправильно:

glUseProgram(program); 
TextureSetup(program, 1, "colorTexture", texId1);
TextureSetup(program, 1, "otherTexture", texId2);

Ошибки, наиболее часто совершаемые при изучении OpenGL

OpenGL – это конечный автомат. Но конечный автомат, имеющий свою спецификацию (a href="http://www.opengl.org/sdk/docs/man3/">http://www.opengl.org/sdk/docs/man3/).

Обязательно изучите эту страницу http://www.opengl.org/wiki/Common_Mistakes.

Если у вас что-то не работает, то последовательность действий должна быть следующая:

  1. После каждого вызова OpenGL ставите OGL_CHECK_FOR_ERRORS; (лучше ставить его вседа).
  2. Получаете код ошибки (например, у вас падает на операции glUniform1i с ошибкой GL_INVALID_OPERATION).
  3. Идете в документацию http://www.opengl.org/sdk/docs/man3/
  4. Проверяете последовательно все причины, по которым могла сгенерироваться эта ошибка для glUniform1i:
    1. GL_INVALID_OPERATION is generated if there is no current program object.
    2. GL_INVALID_OPERATION is generated if the size of the uniform command.
    3. GL_INVALID_OPERATION is generated if one of the signed …
  5. Если нашли, то исправляете, если все же не можете понять в чем дело, спросите на нашем форуме или здесь http://gamedev.ru. На этом форуме находится много людей, постоянно имеющих дело с OpenGL самых разных версий.
  6. Всегда старайтесь разобраться, в чем проблема, не пытайтесь написать код в стиле “авось да заработает”.

Cсылки

[0] http://www.arcsynthesis.org/gltut/
[1] http://steps3d.narod.ru/tutorials/geometry-shader-tutorial.html
[2] http://code.google.com/p/gl33lessons/wiki/Lesson07
[3] http://www.opengl.org/sdk/docs/man3/
[4] http://code.google.com/p/gl33lessons/wiki/Lesson07
[5] http://www.opengl.org/wiki/Common_Mistakes
[6] http://steps3d.narod.ru/tutorials/

© Лаборатория компьютерной графики при ВМиК МГУ