netlib.narod.ru< Назад | Оглавление | Далее >

Шейдеры шаг за шагом

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

Чтобы увидеть, к чему мы стремимся, взгляните на рис. 6.4, где показана модель яблока из предыдущей главы, но теперь к ней применен ваш новый шейдер SimpleShader.fx и используется другая текстура с небольшим текстом на ней.


Рис. 6.4

Рис. 6.4


SimpleShader.fx использует обычный вершинный шейдер, который просто преобразует трехмерные данные, и простую попиксельную технику отражаемого освещения для пиксельного шейдера, которая обрабатывает каждый пиксел и вычисляет фоновый, рассеиваемый и отражаемый компоненты освещения для него. Такого рода вычисления невозможны с шейдером фиксированных функций, поскольку вы не можете запрограммировать какую-нибудь особую формулу для пиксельного шейдера, и вам остается только показывать эффекты отражаемого света, выполняя их в вершинном шейдере. Вы можете спросить, почему плохо рассчитывать отражаемый компонент освещения в вершинном шейдере. Дело в том, что если у вас низкополигонный объект, такой как яблоко здесь или сфера из примера наложения нормалей, результат вычисления цветовых компонентов в вершинном шейдере будет выглядеть плохо, поскольку результирующие цвета вычисляются только для каждой из вершин. Взглянув на каркас модели яблока (рис. 6.5) вы увидите, что в нем всего пригоршня вершин и гораздо больше пикселей, заполняющих промежутки между ними. Если проводить вычисления только для вершин (точек где соединяются ребра), все данные между ними вычисляются не по правильным формулам, а получаются путем интерполяции. Показанная на рис. 6.4 подсветка между вершинами будет вообще невидима. Кстати, если вы хотите отобразить что-нибудь в виде каркаса, просто добавьте следующую строку перед визуализацией каких-либо трехмерных данных:

BaseGame.Device.RenderState.FillMode = FillMode.WireFrame;

Рис. 6.5

Рис. 6.5


FX Composer

Чтобы начать работу над нашим маленьким шейдером SimpleShader.fx мы воспользуемся свободно доступной утилитой FX Composer, которую вы можете загрузить с домашней страницы Nvidia (www.Nvidia.com) из раздела для разработчиков (или просто поищите в Google).

Установив и запустив FX Composer вы увидите экран, показанный на рис. 6.6. В окне программы есть несколько панелей, которые могут использоваться в том числе и художниками для проверки их текстур и шейдерных техник, а также для модификации параметров шейдеров, таких как цветовые значения, интенсивность эффекта и т.д. Для вас, как разработчика, наиболее важна панель в центре, которая отображает исходный код выбранного в данный момент файла .fx (аналогично Visual Studio). Как видите, код файла .fx выглядит очень похоже на C# или C++, и в нем также выполняется подсветка синтаксиса для ключевых слов, которые также аналогичны C#. Например, string — это текстовая строка, а float — это число с плавающей точкой и т.д., также как и в C#. Но есть и другие типы, такие как float3 или float4x4. Числа за словом float просто указывают размерность; float3 это та же структура, что и Vector3 в XNA, содержащая три значения с плавающей точкой (x, y и z). Тип float4x4 определяет матрицу, содержащую 16 значений с плавающей точкой (4 × 4); это тот же формат, что у структуры Matrix в XNA. Последний тип переменных, с которым следует познакомиться, — это текстуры. Текстуры определяются с указанием типа texture и, кроме этого, вы должны определить механизм выборки, сообщающий шейдеру как использовать эту текстуру (какой вид фильтрации использовать, какие размеры имеет текстура и т.д.).


Рис. 6.6

Рис. 6.6


Последняя, но не менее важная вещь, — необязательные семантические значения (такие, как WorldViewProjection, Diffuse, Direction и т.д.) следующие за объявлением переменной через двоеточие. Они сообщают FX Composer как заполнять эти значения и как использовать их. Для ваших программ XNA эти значения не важны, но обычно семантика всегда определяется. Это позволяет вам использовать шейдер в других программах, таких как FX Composer, 3D Studio Max, а также поможет когда вы будете читать код шейдера через какое-то время. Семантика точно говорит вам для какой цели была предназначена данная переменная. В XNA нет ограничений на использование; вы, например, можете установить мировую матрицу в viewInverse, но позднее это приведет к жуткой путанице, не так ли?

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

Структура файла FX

Если вы раньше работали с OpenGL и самостоятельно писали свои вершинные и фрагментные шейдеры (это то же, что вершинные и пиксельные шейдеры в DirectX), то будете счастливы услышать, что файлы .fx позволяют иметь код всех этих шейдеров в одном месте и у вас в вашем файле .fx может быть много различных блоков пиксельных шейдеров для поддержки нескольких целевых конфигураций, таких как пиксельные шейдеры 1.1 для GeForce 3 и поддержка пиксельных шейдеров 2.0 для Nvidia GeForce FX или серии ATI Rageon 9x. Другой полезной стороной эффектов является возможность поддерживать несколько вершинных и пиксельных шейдеров в одном файле .fx, что позволяет объединить похожие шейдеры вместе и позволить всем им использовать общие методы и одинаковые параметры, а это значительно упрощает разработку шейдеров. Например, если у вас есть шейдер наложения нормалей для блестящей металлической поверхности, который действительно здорово выглядит на металле, вы, возможно, захотите иметь другой, выглядящий более размытым, шейдер для камня, и вы можете поместить другую шейдерную технику в тот же файл шейдера, а затем выбирать используемую технику в вашем движке в зависимости от материала, который собираетесь отображать.

На рис. 6.7 показана структура файла SimpleShader.fx в качестве примера типичного файла .fx. В более сложных шейдерах может быть больше вершинных и пиксельных шейдеров и больше техник. Вам не надо создавать отдельные вершинные и пиксельные шейдеры для каждой техники; их можно комбинировать любым желаемым способом. Некоторые шейдеры могут также использовать несколько проходов; это значит, что они визуализируют все в первом проходе, а затем во втором проходе снова визуализируют те же самые данные, добавляя больше эффектов или слоев к материалам. Использование нескольких проходов обычно слишком медленно для приложений, работающих в реальном времени, поскольку визуализация с 10 проходами означает, что на визуализацию уйдет в 10 раз больше времени, чем при визуализации одного прохода с тем же самым шейдером. В ряде случаев использование нескольких проходов может быть полезным для достижения эффектов, которые в ином случае не помешаются в лимит инструкций шейдера, или для постэкранных шейдеров, которые берут результат первого прохода и модифицируют его во втором проходе для достижения лучших эффектов. Например, размытие требует большого количества инструкций, поскольку чтобы получить хорошо размытый результат надо смешать много пикселов для вычисления каждой размытой точки.


Рис. 6.7

Рис. 6.7


Например, размытие с областью выборки 10 × 10 пикселов требует 100 инструкций чтения пикселов, и это звучит не слишком хорошо, если у вас пара миллионов пикселей, когда вы хотите размыть изображение на всем экране. В этом случае гораздо лучше сначала выполнить размытие по оси X (10 инструкций чтения пикселов), получить результат и размыть его по оси Y во втором проходе с другим набором из 10 инструкций чтения пикселов. Теперь ваш шейдер работает в пять раз быстрее, а выглядит почти так же. Вы можете еще больше увеличить производительность, сперва сжав исходное фоновое изображение от 1600 × 1200 до 400 × 300, и только потом выполнив размытие, что дает шестнадцатикратное увеличение производительности (это поразительно).

О постэкранных шейдерах рассказывает глава 8; но сперва напишем файл SimpleShader.fx. Как видно на рис. 6.7, шейдер использует достаточно много параметров. Некоторые из них не слишком важны, поскольку вы можете жестко запрограммировать параметры материала непосредственно внутри шейдера, но используемый подход позволяет менять цвет и внешний вид материала в вашем движке и использовать шейдер для множества различных материалов. Другие параметры, такие как матрицы и текстуры, очень важны и вы не можете использовать шейдер, если эти параметры не установлены движком. Данные материала, такие как цветовые значения и текстуры должны быть загружены в то время, когда вы создаете шейдер в вашем движке, а мировая матрица, направление света и т.д. должны устанавливаться в каждом кадре, потому что они постоянно изменяются.

Параметры

Если вы хотите точно следовать этапам создания этого шейдера, запустите сейчас FX Composer и начните с пустого файла .fx. Выберите в меню File команду New для создания нового пустого файла .fx и удаления всего содержимого. Мы начинаем с полностью чистого листа.

Сперва мы добавим описание или комментарий в начале файла, чтобы быстро вспомнить, для чего предназначен этот файл, когда откроем его позже:

// Глава 6: Написание простого шейдера для XNA

Как вы видели в обзоре SimpleShader.fx, вам необходимо несколько матриц для преобразования трехмерных данных в вершинном шейдере. Это будет матрица worldViewProj, матрица world и матрица viewInverse. Матрица worldVievProj является комбинацией мировой матрицы, которая помещает объект, который вы хотите визуализировать, в правильную позицию в мире, матрицы вида, которая преобразует трехмерные данные в пространство вида вашей камеры (смотрите рис. 5.7 из предыдущей главы) и матрицы проекции, которая помещает точки из пространства вида на соответствующие им места экрана. Эта матрица позволяет быстро преобразовать исходную позицию в итоговую позицию на экране с помощью единственной операции умножения матриц. Матрица world используется для выполнения различных операций в трехмерном мире, таких как вычисление мировых нормалей, расчет освещения и многого другого. Матрица viewInverse обычно используется только для получения дополнительной информации о местоположении камеры, которое может быть извлечено из четвертой строки этой матрицы:

float4x4 worldViewProj : WorldViewProjection;
float4x4 world : World;
float4x4 viewInverse : ViewInverse;

Каждая из этих матриц имеет тип float4x4 (это тот же тип данных, что и Matrix в XNA) и вы используете семантику шейдера для лучшего описания этих значений и чтобы получить поддержку в таких приложениях, как FX Composer или 3D Studio Max, что важно, когда создатель моделей захочет увидеть, как будет выглядеть его трехмерная модель с вашим шейдером. Самое замечательное, что все будет выглядеть абсолютно одинаково, независимо от того, смотрите ли вы в FX Composer, 3D Studio или в вашем движке. Этот факт экономит много времени разработчиков игр; особенно сокращается процесс тестирования, обеспечивающий, чтобы все трехмерные объекты выглядели правильно.

Пришло время сохранить ваш файл. Просто щелкните по кнопке Build или нажмите Ctrl+F7, и вас попросят ввести имя для нового шейдера. Назовите его SimpleShader.fx и поместите в ваш каталог содержимого XnaGraphicsEngine, чтобы позже его можно было быстро использовать в XNA. После сохранения FX Composer сообщит вам в панели Tasks, расположенной под исходным кодом, что «There were no techniques» и «Compilation failed». Все правильно, техники мы реализуем позже, а сейчас займемся реализацией оставшихся параметров. Поскольку ваш шейдер использует для освещения яблока источник света (рис. 6.4), вам необходим источник света, который может быть либо точечным, либо направленным. Использовать точечное освещение более сложно, поскольку вам надо вычислять направление света для каждой отдельной вершины (или даже для каждого отдельного пикселя, если пожелаете). Если вы используете зональный свет, вычисления становятся еще более сложными. Другой проблемой точечных источников света является то, что обычно их свет с увеличением расстояния затухает, и если ваш трехмерный мир большой, вам понадобится несколько источников света. Направленный свет значительно проще и очень полезен для быстрой имитации солнечного света во внешней среде, подобной используемой в игре, создаваемой в следующей главе.

float3 lightDir : Direction
<
  string Object = "DirectionalLight";
  string Space  = "World";
> = { 1, 0, 0 };

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

Показанный выше код выглядит немного сложнее, но первая строка очень похожа на объявление матриц. Здесь float3 указывает, что вы используете тип Vector3, а Direction сообщает вам, что lightDir используется для направленного света. Затем, внутри фигурных скобок, определены переменные Object и Space. Эти переменные называются аннотациями (annotation) и задают использование параметров для FX Composer или других внешних программ, таких как 3D Studio Max. Теперь программы знают, как использовать эти значения и могут автоматически назначить им объект освещения, который, возможно, уже присутствует на сцене. Таким образом вы можете просто загрузить шейдер в программу трехмерного моделирования и он будет сразу работать без необходимости ручного связывания всех источников света, параметров материалов или текстур.

Затем вы переходите к определению параметров материала; мы будем использовать те же самые параметры, что и у стандартного материала в DirectX. Благодаря этому вы сможете использовать подобные шейдеры или старые материалы DirectX в программах, аналогичных 3D Studio Max, и все цветовые значения будут автоматически правильно подставлены. В движке вы обычно просто устанавливаете фоновый и рассеиваемый цвета и иногда также задаете другое значение яркости для вычисления отражаемого цвета. Вы можете обратить внимание, что здесь вообще не используются аннотации — вы, конечно, можете задать их и здесь, но параметры материалов замечательно работают как в FX Composer, так и в 3D Studio Max и без определения аннотаций. Движок также может использовать значения по умолчанию, если вы не захотите переопределить их позднее в ваших тестовых модулях.

float4 ambientColor : Ambient = { 0.2f, 0.2f, 0.2f, 1.0f };
float4 diffuseColor : Diffuse = { 0.5f, 0.5f, 0.5f, 1.0f };
float4 specularColor : Specular = { 1.0, 1.0, 1.0f, 1.0f };
float shininess : SpecularPower = 24.0f;

Последняя, но не менее важная, потребность вашего шейдера, — текстура, чтобы результат выглядел поинтереснее, чем скучная серая сфера или яблоко. Вместо того, чтобы как в прошлой главе использовать оригинальную текстуру яблока из модели, мы будем применять новую тестовую текстуру, которая станет более интересной в следующей главе, когда мы добавим наложение нормалей. Текстура называется marble.dds (рис. 6.8).

texture diffuseTexture : Diffuse
<
  string ResourceName = "marble.dds";
>;
sampler DiffuseTextureSampler = sampler_state
{
  Texture = <diffuseTexture>;
  MinFilter=linear;
  MagFilter=linear;
  MipFilter=linear;
};

Рис. 6.8

Рис. 6.8


Аннотация ResourceName используется только в FX Composer и автоматически загружает файл marble.dds из того же каталога, где находится шейдер (убедитесь, что файл marble.dds есть также в каталоге содержимого XnaGraphicsEngine). Объект выборки просто указывает, что вы хотите использовать для текстуры линейную фильтрацию.

Входной формат вершин

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

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

struct VertexInput
{
  float3 pos : POSITION;
  float2 texCoord : TEXCOORD0;
  float3 normal : NORMAL;
};

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


Рис. 6.9

Рис. 6.9


Структура VertexOutput вашего шейдера передает преобразованные координаты местоположения вершины, координаты текстуры для используемой текстуры, нормаль и вектор halfVec для вычисления отражаемого цвета непосредственно в пиксельном шейдере. Оба вектора передаются как координаты текстуры, поскольку из вершинного в пиксельный шейдер могут передаваться только координаты местоположения, цвет или координаты текстуры. Но это не имеет значения; вы можете использовать данные таким же образом, что и в структуре VertexInput. Очень важно использовать корректную семантику (Position, TexCoord0 и Normal) в структуре VertexInput, чтобы сообщить FX Composer, вашему приложению или любой другой программе, как использовать шейдер.

Поскольку структуру VertexOutput вы определяете самостоятельно и используете ее только внутри шейдера, вы можете помещать в нее все, что пожелаете, но должны стараться сохранить ее настолько короткой, насколько возможно, и также вы ограничены в количестве координат текстур, которые вы можете передать в пиксельный шейдер (четыре для пиксельных шейдеров 1.1, восемь в пиксельных шейдерах 2.0).

struct VertexOutput
{
  float4 pos : POSITION;
  float2 texCoord : TEXCOORD0;
  float3 normal : TEXCOORD1;
  float3 halfVec : TEXCOORD2;
};

Вершинный шейдер

Вершинный шейдер получает данные из VertexInput и преобразует их в экранную позицию для пиксельного шейдера, который в итоге визуализирует выходной пиксел для каждой видимой точки полигона. Первые строки обычно выглядят похоже для большинства вершинных шейдеров, но вы часто вычисляете значения в конце вершинного шейдера вычисляете значения для использования в пиксельном шейдере. Если вы используете пиксельные шейдеры 1.1, то не можете сделать определенные вещи, такие как нормализация векторов, или вычислить сложные математические функции, такие как возведение в степень. Но даже если вы используете пиксельные шейдеры 2.0 (как в рассматриваемом шейдере), лучше предпочесть заранее вычислять некоторые значения для ускорения работы пиксельного шейдера, который выполняется для каждой отдельной видимой точки. Обычно у вас гораздо меньше вершин, чем пикселов, и каждое сложное вычисление, выполненное в вершинном шейдере может увеличить производительность пиксельного шейдера в несколько раз.

// Вершинный шейдер
VertexOutput VS_SpecularPerPixel(VertexInput In)
{
  VertexOutput Out = (VertexOutput)0;
  float4 pos = float4(In.pos, 1);
  Out.pos = mul(pos, worldViewProj);
  Out.texCoord = In.texCoord;
  Out.normal = mul(In.normal, world);

  // Позиция зрителя
  float3 eyePos = viewInverse[3];

  // Мировая позиция
  float3 worldPos = mul(pos, world);

  // Вектор взгляда
  float3 eyeVector = normalize(eyePos-worldPos);

  // Средний вектор
  Out.halfVec = normalize(eyeVector+lightDir);

  return Out;
} // VS_SpecularPerPixel(In)

Вершинный шейдер получает структуру VertexInput в качестве параметра который автоматически заполняется трехмерными данными из вашего приложения и передается при помощи шейдерной техники, которую вы определите позже, в конце файла .fx. Важная часть здесь — это структура VertexOutput, которая возвращается из вершинного шейдера и затем передается в пиксельный шейдер. Данные не просто передаются пиксельному шейдеру в точном соответствии, но все значения интерполируются для каждой отдельной точки полигона (рис. 6.10).


Рис. 6.10

Рис. 6.10


Конечно, это хорошо для координат и цветовых значений, поскольку результат выглядит гораздо лучше, когда значения корректно интерполируются. Но если вы используете нормализованные векторы, они могут исказиться в процессе автоматически выполняемой GPU интерполяции. Чтобы исправить это вы повторно нормализуете векторы в пиксельном шейдере (рис 6.11). Иногда это можно игнорировать, потому что артефакты невидимы, но при попиксельном вычислении отражаемого освещения они будут заметны на любом низкополигональном объекте. Если вы используете пиксельные шейдеры 1.1, то не можете в пиксельном шейдере использовать метод normalize. Вместо этого вы можете использовать вспомогательную кубическую карту, которая содержит заранее вычисленные нормализованные значения для каждого возможного входного значения. Чтобы разобраться в этом подробнее, взгляните на шейдерные эффекты NormalMapping и ParallaxMapping в следующей главе.


Рис. 6.11

Рис. 6.11


Если вы снова быстро взглянете на исходный код, то заметите, что он начинается с вычисления итоговой позиции на экране. Поскольку все матричные операции ожидают значения Vector4, вы преобразуете входное значение Vector3 в Vector4 и присваиваете компоненту w значение 1, чтобы получить поведение по умолчанию отвечающей за перемещение части матрицы worldViewProj.

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

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

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


Рис. 6.12

Рис. 6.12


Пиксельный шейдер

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

// Пиксельный шейдер
float4 PS_SpecularPerPixel(VertexOutput In) : COLOR
{
  return float4(1, 0, 0, 1);
} // PS_SpecularPerPixel(In)

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

technique SpecularPerPixel
{
  pass P0
  {
    VertexShader = compile vs_2_0 VS_SpecularPerPixel();
    PixelShader = compile ps_2_0 PS_SpecularPerPixel();
  } // проход P0
} // SpecularPerPixel

Теперь вы, наконец-то, в состоянии скомпилировать шейдер в FX Composer, и должны увидеть результат, показанный на рис. 6.13. Убедитесь, что шейдер назначен для применения в панели Scene FX Composer (щелкните по сфере, затем щелкните по материалу SimpleShader.fx в панели материалов и щелкните по кнопке Apply Material).


Рис. 6.13

Рис. 6.13


Следующий шаг заключается в помещении на сферу текстуры marble.dds. Это делается в пиксельном шейдере с помощью метода tex2D, который ожидает в первом параметре объект выборки текстуры, а во втором — координаты текстуры. Чтобы текстурировать ваш трехмерный объект, замените строку return float4 из предыдущего кода на показанный ниже код:

float4 textureColor = tex2D(DiffuseTextureSampler, In.texCoord);
return textureColor;

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


Рис. 6.14

Рис. 6.14


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

// Пиксельный шейдер
float4 PS_SpecularPerPixel(VertexOutput In) : COLOR
{
  float4 textureColor = tex2D(DiffuseTextureSampler, In.texCoord);
  float3 normal = normalize(In.normal);
  float brightness = dot(normal, lightDir);
  float specular = pow(dot(normal, In.halfVec), shininess);
  return textureColor *
         (ambientColor + brightness * diffuseColor) +
         specular * specularColor;
} // PS_SpecularPerPixel(In)

Рассеиваемый цвет вычисляется путем вычисления скалярного произведения повторно нормализованной нормали (в мировом пространстве, смотри обсуждение вершинного шейдера выше) и вектора lightDir, который также в мировом пространстве. При вычислении матриц, скалярного или векторного произведения очень важно, чтобы все операнды находились в одном пространстве, иначе результаты будут абсолютно неверными. Скалярное произведение ведет себя именно так, как вам надо для вычисления рассеиваемого цвета. Если lightDir и нормаль указывают в одном и том же направлении, это значит, что нормаль направлена прямо на солнце и интенсивность рассеиваемого цвета должна быть максимальной (1.0); если нормаль расположена под углом 90 градусов, скалярное произведение возвращает 0 и рассеиваемый компонент равен нулю. Чтобы видеть и темные стороны сферы, добавлен фоновый свет, который освещает сферу там, где ни рассеиваемый, ни отражаемый свет невидимы.

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


Рис. 6.15

Рис. 6.15


Теперь вы закончили работу над шейдером. Сохраненный файл шейдера может использоваться в других программах, таких как 3D Studio Max, чтобы художники могли увидеть как их трехмерные модели будут выглядеть в игровом движке. Теперь вы переходите к реализации шейдера в вашем движке XNA.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz