Введение в высокоуровневый язык шейдеров
В этой главе мы рассмотрим высокоуровневый язык программирования шейдеров (High-Level Shading Language, HLSL), который будем использовать для программирования вершинных и пиксельных шейдеров в следующих трех главах. Если коротко, вершинные и пиксельные шейдеры — это небольшие программы, которые вы пишете сами, и которые выполняются процессором видеокарты, заменяя часть функций фиксированного конвейера. Возможность заменять функции фиксированного конвейера собственными программами шейдеров открывает перед нами необозримые возможности реализации различных визуальных эффектов. Мы больше не ограничены предопределенными «фиксированными» операциями.
Для того, чтобы написать программу шейдера, нам необходим позволяющий сделать это язык программирования. В DirectX 8.x программы шейдеров можно было писать только на низкоуровневом языке ассемблера шейдеров. К счастью, нам больше не требуется писать шейдеры на языке ассемблера, поскольку DirectX 9 предоставляет предназначенный для написания шейдеров высокоуровневый язык программирования шейдеров (High-Level Shading Language). Использование для написания программ шейдеров HLSL вместо языка ассемблера дает те же преимущества, что и использование высокоуровневого языка, такого как С++, вместо ассемблера при написании приложений:
Увеличивается производительность труда — писать программы на высокоуровневом языке быстрее и проще, чем на низкоуровневом. Мы можем больше времени уделить разработке алгоритмов, а не кодированию.
Улучшается читаемость программ — программы на высокоуровневом языке проще читать, а значит их проще отлаживать и поддерживать.
Компиляторы часто генерируют боле эффективный ассемблерный код, чем тот, который вы сможете написать сами.
Используя компилятор HLSL мы можем компилировать код для любой доступной версии шейдеров. Если мы пользуемся языком ассемблера, то должны портировать код для каждой версии, поддержка которой нам требуется.
Синтаксис HLSL очень похож на синтакс C и C++, так что изучить этот язык будет достаточно просто.
Учтите, что если ваша видеокарта не поддерживает вершинные и пиксельные шейдеры, то для использующих шейдеры программ в коде надо выбирать устройство REF. Использование устройства REF означает, что примеры работы с шейдерами будут выполняться очень медленно, но при этом на экране будет отображаться корректный результат, позволяющий убедиться в правильности написанного кода.
ПРИМЕЧАНИЕ
Цели |
Узнать как написать и скомпилировать программу шейдера на HLSL. Изучить, как выполняется обмен данными между приложением и программой шейдера. Познакомиться с синтаксисом, типами и встроенными функциями HLSL. |
Глобальные переменные
16.1.1. Глобальные переменные
Сначала мы объявляем две глобальные переменные:
matrix ViewProjMatrix; vector Blue = {0.0f, 0.0f, 1.0f, 1.0f};
Первая переменная, ViewProjMatrix, относится к типу matrix, который представляет матрицы 4 × 4 и является встроенным типом HLSL. Эта переменная хранит комбинацию матриц вида и проекции и, следовательно, описывает оба эти преобразования. Благодаря комбинированию преобразований мы обойдемся одной операцией умножения вектора на матрицу вместо двух. Обратите внимание, что нигде в исходном коде шейдера нет инициализации этой переменной. Это объясняется тем, что мы инициализируем данную переменную из приложения, а не в шейдере. Обмен данными между приложением и программой шейдера является одной из наиболее часто используемых операций, и ее исследованию посвящен раздел 16.2.1.
Вторая переменная, Blue, относится к встроенному типу vector, который представляет четырехмерный вектор. Мы просто инициализируем его компоненты для синего цвета, рассматривая его как цветовой вектор RGBA.
Мы пишем программы на HLSL
Мы пишем программы на HLSL в текстовом редакторе, сохраняем их в обычном текстовом файле ASCII, а затем компилируем их в нашем приложении с помощью функции D3DXCompileShaderFromFile.
Интерфейс ID3DXConstantTable позволяет нам устанавливать значения переменных шейдеров из нашего приложения. Такое взаимодействие необходимо, поскольку значения используемых в шейдерах переменных могут меняться от кадра к кадру. Например, если в приложении изменяется матрица вида, нам необходимо обновить и переменную с матрицей вида в шейдере, записав в нее новые значения. Эти действия и позволяет выполнить интерфейс ID3DXConstantTable.
Для каждого шейдера мы должны определить входную и выходную структуры, описывающие формат данных, которые шейдер получает и которые возвращает соответственно.
У каждого шейдера есть функция, являющаяся точкой входа, которая в параметре получает входную структуру, использующуюся для передачи данных в шейдер. Кроме того, каждый шейдер возвращает экземпляр выходной структуры, использующийся для возврата данных из шейдера.
Ключевое слово typedef
16.3.6. Ключевое слово typedef
Ключевое слово typedef делает в HLSL то же самое, что и в С++. Например, приведенный ниже фрагмент кода присваивает имя point типу vector<float,3>:
typedef vector<float, 3> point;
Теперь вместо
vector<float, 3> myPoint;
мы можем писать
point myPoint;
Вот еще два примера, показывающие как можно использовать ключевое слово typedef с константными типами и массивами:
typedef const float CFLOAT; typedef float point2[2];
Ключевые слова
16.4.1. Ключевые слова
Для справки мы приводим здесь ключевые слова, определенные в HLSL:
asm bool compile const decl do double else extern false float for half if in inline inout int matrix out pass pixelshader return sampler shared static string struct technique texture true typedef uniform vector vertexshader void volatile while
Во втором списке приведены идентификаторы, которые зарезервированы и в данный момент не используются, но в будущих версиях могут стать ключевыми словами:
auto break case catch char class const_cast continue default delete dynamic_cast enum explicit friend goto long mutable namespace new operator private protected public register reinterpret_cast short signed sizeof static_cast switch template this throw try typename union unsigned using virtual
Компиляция HLSL-шейдера
16.2.2. Компиляция HLSL-шейдера
Мы можем скомпилировать шейдер, код которого хранится в текстовом файле, с помощью следующей функции:
HRESULT D3DXCompileShaderFromFile( LPCSTR pSrcFile, CONST D3DXMACRO* pDefines, LPD3DXINCLUDE pInclude, LPCSTR pFunctionName, LPCSTR pTarget, DWORD Flags, LPD3DXBUFFER* ppShader, LPD3DXBUFFER* ppErrorMsgs, LPD3DXCONSTANTTABLE* ppConstantTable );
pSrcFile — Имя текстового файла, содержащего исходный код шейдера, который вы хотите скомпилировать.
pDefines — Необязательный параметр, и в данной книге мы всегда будем указывать в нем null.
pInclude — Указатель на интерфейс ID3DXInclude. Этот интерфейс разработан для тех приложений, которым требуется переопределить устанавливаемое по умолчанию поведение включения. В общем случае поведение по умолчанию замечательно работает и поэтому мы игнорируем данный параметр, передавая в нем null.
pFunctionName — Строка, задающая имя функции, являющейся точкой входа. Например, если точкой входа шейдера является функция с именем Main, мы должны передать в этом параметре строку «Main».
pTarget — Строка, задающая версию шейдеров для которой будет компилироваться исходный код HLSL. Для вершинных шейдеров доступны версии vs_1_1, vs_2_0, vs_2_sw. Для пиксельных шейдеров доступны версии ps_1_1, ps_1_2, ps_1_3, ps_1_4, ps_2_0, ps_2_sw. Например, если мы хотим чтобы наш вершинный шейдер был скомпилирован для версии 2.0, надо указать в этом параметре vs_2_0.
ПРИМЕЧАНИЕ
Flags — Необязательные флаги компиляции; если флаги не нужны, укажите 0. Можно использовать следующие значения:
D3DXSHADER_DEBUG — Приказывает компилятору включать в скомпилированный файл отладочную информацию.
D3DXSHADER_SKIPVALIDATION — Приказывает компилятору не выполнять проверку корректности кода. Этот флаг следует использовать только при работе с теми шейдерами в правильности кода которых вы абсолютно уверены.
D3DXSHADER_SKIPOPTIMIZATION — Приказывает компилятору не выполнять оптимизацию кода. Обычно этот флаг используется при отладке, когда вы не хотите, чтобы компилятор вносил какие-либо изменения в код.
ppShader — Возвращает указатель на интерфейс ID3DXBuffer, который содержит скомпилированный код шейдера. Этот скомпилированный код затем передается в параметре другой функции, которая выполняет фактическое создание вершинного или пиксельного шейдера.
ppErrorMsgs — Возвращает указатель на интерфейс ID3DXBuffer, содержащий строку с кодами обнаруженых при компиляции ошибок и их описанием.
ppConstantTable — Возвращает указатель на интерфейс ID3DXConstantTable, содержащий данные таблицы констант шейдера.
Вот пример вызова функции D3DXCompileShaderFromFile:
// // Компиляция шейдера // ID3DXConstantTable* TransformConstantTable = 0; ID3DXBuffer* shader = 0; ID3DXBuffer* errorBuffer = 0;
hr = D3DXCompileShaderFromFile( "transform.txt", // имя файла шейдера 0, 0, "Main", // имя точки входа "vs_2_0", // версия шейдеров D3DXSHADER_DEBUG, // компиляция для отладки &shader, &errorBuffer, &TransformConstantTable);
// Выводим сообщения об ошибках if(errorBuffer) { ::MessageBox(0, (char*)errorBuffer->GetBufferPointer(), 0, 0); d3d::Release<ID3DXBuffer*>(errorBuffer); }
if(FAILED(hr)) { ::MessageBox(0, "D3DXCreateEffectFromFile() - FAILED", 0, 0); return false; }
Массивы
16.3.4. Массивы
Мы можем объявить массив значений заданного типа используя синтаксис, аналогичный C++. Например:
float M[4][4]; half p[4]; vector v[12];
Матричные типы
16.3.3. Матричные типы
HLSL предоставляет следующие встроенные матричные типы:
matrix — Матрица 4 × 4 все элементы которой имеют тип float.
matrix<T, m, n> — Матрица размера m × n, каждый элемент которой относится к скалярному типу T. Размеры матрицы m и n могут принимать значения от 1 до 4. Вот пример матрицы целых чисел, размером 2 × 2:
matrix<int, 2, 2> m2x2;
Кроме того, мы можем объявить матрицу m × n, где m и n — числа в диапазоне от 1 до 4, используя следующий синтаксис:
floatmxn matmxn;
Примеры:
float2x2 mat2x2; float3x3 mat3x3; float4x4 mat4x4; float2x4 mat2x4;
ПРИМЕЧАНИЕ
int2x2 i2x2; int2x2 i3x3; int2x2 i2x4;
Мы можем обращаться к элементам матрицы, используя синтаксис доступа к элементу массива по двум индексам. Например, для установки значения элемента (i, j) матрицы M, следует писать:
M[i][j] = value;
Кроме того, мы можем обращаться к элементам матрицы M как к членам структуры. Определены следующие имена элементов:
Если нумерация начинается с единицы:
M._11 = M._12 = M._13 = M._14 = 0.0f; M._21 = M._22 = M._23 = M._24 = 0.0f; M._31 = M._32 = M._33 = M._34 = 0.0f; M._41 = M._42 = M._43 = M._44 = 0.0f;
Если нумерация начинается с нуля:
M._m00 = M._m01 = M._m02 = M._m03 = 0.0f; M._m10 = M._m11 = M._m12 = M._m13 = 0.0f; M._m20 = M._m21 = M._m22 = M._m23 = 0.0f; M._m30 = M._m31 = M._m32 = M._m33 = 0.0f;
Иногда нам будет требоваться сослаться на отдельный вектор-строку матрицы. Это делается с использованием синтаксиса доступа к элементу массива по индексу. Например, чтобы получить i-ый вектор-строку матрицы M, следует написать:
vector ithRow = M[i]; // получить i-ый вектор-строку в M
ПРИМЕЧАНИЕ
vector u = {0.6f, 0.3f, 1.0f, 1.0f}; vector v = {1.0f, 5.0f, 0.2f, 1.0f};
Или эквивалентный стиль конструктора:
vector u = vector(0.6f, 0.3f, 1.0f, 1.0f); vector v = vector(1.0f, 5.0f, 0.2f, 1.0f);
Вот еще несколько примеров:
float2x2 f2x2 = float2x2(1.0f, 2.0f, 3.0f, 4.0f); int2x2 m = {1, 2, 3, 4}; int n = int(5); int a = {5}; float3 x = float3(0, 0, 0);
Операторы
HLSL поддерживает иного операторов, похожих на те, что есть в C++. За исключением нескольких особенностей, о которых мы поговорим ниже, эти операторы работают точно так же, как и их аналоги в C++. Ниже приведен список поддерживаемых в HLSL операторов:
[] . > < <= >= != == ! && || ?: + += – –= * *= / /= % %= ++ –– = () ,
Хотя поведение операторов очень похоже на С++, есть несколько отличий. Во-первых, операция деления по модулю работает как для целых чисел, так и для чисел с плавающей точкой. Кроме того, для операции деления по модулю необходимо, чтобы у обоих ее операндов был один и тот же знак (то есть, чтобы либо оба операнда были положительными, либо оба отрицательными).
Во-вторых, обратите внимание, что большинство операторов HLSL действуют покомпонентно. Это вызвано тем фактом, что векторы и матрицы встроены в язык, а эти типы состоят из отдельных компонентов. Благодаря наличию операторов, работающих на уровне компонентов, такие операции как сложение векторов/матриц, вычитание векторов/матриц и проверка равенства векторов/матриц выполняются с использованием тех же операторов, которые применяются для скалярных типов. Взгляните на следующие примеры:
ПРИМЕЧАНИЕ
vector u = {1.0f, 0.0f, -3.0f, 1.0f}; vector v = {-4.0f, 2.0f, 1.0f, 0.0f};
// Складываем соответствующие компоненты vector sum = u + v; // сумма = (-3.0f, 2.0f, -2.0f, 1.0f)
Инкремент вектора увеличивает каждую из его компонент:
// до инкремента: sum = (-3.0f, 2.0f, -2.0f, 1.0f)
sum++; // после инкремента: sum = (-2.0f, 3.0f, -1.0f, 2.0f)
Покомпонентное произведение векторов:
vector u = {1.0f, 0.0f, -3.0f, 1.0f}; vector v = {-4.0f, 2.0f, 1.0f, 0.0f};
// Умножаем соответствующие компоненты vector sum = u * v; // произведение = (-4.0f, 0.0f, -3.0f, 0.0f)
Операторы сравнения тоже работают покомпонентно и возвращают вектор или матрицу, каждый элемент которой является логическим значением. Полученный в результате «логический» вектор содержит результаты сравнения соответствующих компонент его операндов.
Например:
vector u = { 1.0f, 0.0f, -3.0f, 1.0f}; vector v = {-4.0f, 0.0f, 1.0f, 1.0f};
vector b = (u == v); // b = (false, true, false, true)
И, в заключение, мы рассмотрим повышение типа переменной при бинарных операциях:
Если в бинарной операции размер левого операнда отличается от размера правого операнда, то операнд меньшего размера повышается (приводится) до типа операнда большего размера. Например, если переменная x типа float, а переменная y типа float3, то в выражении (x + y) переменная x будет повышена до типа float3 и результатом всего выражения также будет значение типа float3. При повышении типа используются предопределенные правила приведения типов. В рассматриваемом случае мы преобразуем скаляр в вектор; следовательно, после повышения x до float3, x = (x, x, x), как указано в правилах приведения скалярных типов к векторным. Помните, что результат повышения не определен, если не определена соответствующая операция приведения. Например, мы не можем выполнить повышение float2 до float3 поскольку такая операция приведения типа не существует.
Если в бинарной операции диапазон значений левого операнда отличается от диапазона значений правого операнда, то операнд с меньшим диапазоном значений повышается (приводится) до типа операнда с большим диапазоном значений. Например, если переменная x типа int, а переменная y типа half, то в выражении (x + y) переменная x будет повышена до типа half и результатом всего выражения также будет значение типа half.
Определяемые пользователем функции
Функции в HLSL имеют следующие особенности:
Синтаксис объявления функций такой же, как и в С++.
Параметры всегда передаются по значению.
Рекурсия не поддерживается.
Функции всегда встраиваемые (inline).
Кроме того, в HLSL добавлено несколько дополнительных ключевых слов, которые могут использоваться в объявлениях функций. Для примера, рассмотрим приведенный ниже код функции на HLSL:
bool foo(in const bool b, // Входное значение bool out int r1, // Выходное значение int inout float r2) // Входное и выходное значение float { if( b ) // Проверяем входное значение { r1 = 5; // Возвращаем значение через r1 } else { r1 = 1; // Возвращаем значение через r1 }
// Поскольку r2 объявлена с ключевым словом inout // мы можем использовать ее как входное значение // и возвращать значения через нее r2 = r2 * r2 * r2;
return true; }
Функция почти полностью аналогична коду на C++, за исключением ключевых слов in, out и inout.
in — Указывает что аргумент (конкретная переменная, которую мы передаем в параметре) должен быть скопирован в параметр перед началом выполнения функции. Не требуется явно указывать ключевое слово in, поскольку его наличие подразумевается по умолчанию. Например, следующие две записи эквивалентны:
float square(in float x) { return x * x; }
Без явного указания in:
float square(float x) { return x * x; }
out — Указывает, что при возврате из функции параметр должен быть скопирован в аргумент. Это применяется для возврата значений через параметры. Ключевое слово out необходимо потому что HLSL не поддерживает передачу по ссылке или передачу указателя. Обратите внимание, что если параметр отмечен ключевым словом out, аргумент не копируется в параметр перед началом работы функции. Другими словами, такой параметр может использоваться только для возврата значений и не может применяться для передачи значений в функцию.
void square(in float x, out float y) { y = x * x; }
Здесь мы передаем возводимое в квадрат число через параметр x, а результат вычислений возвращаем через параметр y.
inout — Данное сокращение означает, что параметр является как входным, так и выходным. Ключевое слово inout применяется в том случае, если вам надо использовать один и тот же параметр как для передачи значений в функцию, так и для возврата значений из нее.
void square(inout float x) { x = x * x; }
Здесь мы передаем возводимое в квадрат число через параметр x и через него же возвращаем вычисленное значение.
Пишем шейдер на HLSL
Мы можем написать код нашего HLSL-шейдера непосредственно в исходном коде приложения в виде длинной строки символов. Однако более удобный и правильный подход— разделить код шейдеров и код приложения. Поэтому мы будем писать наши шейдеры в программе Notepad и сохранять их как обычные текстовые файлы ASCII. Затем для компиляции наших шейдеров мы воспользуемся функцией D3DXCompileShaderFromFile (раздел 16.2.2).
В качестве примера, рассмотрим приведенный ниже простой вершинный шейдер, написанный на HLSL и сохраненный в текстовый файл с именем Transform.txt. Полный код проекта находится в папке с именем Transform, расположенной в сопроводительных файлах к данной главе. Вершинный шейдер преобразует вершину путем применения комбинации матриц вида и проекции а также присваивает рассеиваемой составляющей цвета вершины синий цвет.
ПРИМЕЧАНИЕ
///////////////////////////////////////////////////////////////////// // // Файл: transform.txt // // Автор: Фрэнк Д. Луна (C) All Rights Reserved // // Система: AMD Athlon 1800+ XP, 512 DDR, Geforce 3, Windows XP, // MSVC++ 7.0 // // Описание: Вершинный шейдер, преобразующий вершину с помощью // комбинации матриц преобразования вида и проекции и устанавливающий // для вершины синий цвет // /////////////////////////////////////////////////////////////////////
// // Глобальные переменные //
// Глобальная переменная для хранения комбинации // матриц преобразования вида и проекции. // Мы инициализируем эту переменную в приложении. matrix ViewProjMatrix;
// Инициализация глобального вектора для синего цвета vector Blue = {0.0f, 0.0f, 1.0f, 1.0f};
// // Структуры //
// Входная структура описывает вершины, которые будут // передаваться в шейдер. Здесь входная вершина содержит // только данные о местоположении. struct VS_INPUT { vector position : POSITION; };
// Выходная структура описывает вершину, которая // возвращается шейдером. Здесь выходная вершина // содержит данные о местоположении и цвет struct VS_OUTPUT { vector position : POSITION; vector diffuse : COLOR; };
// // Главная Точка Входа. Обратите внимание, // что функция получает в своем параметре копию // входной вершины и возвращает копию вычисленной // выходной вершины. //
VS_OUTPUT Main(VS_INPUT input) { // Обнуляем данные выходной вершины VS_OUTPUT output = (VS_OUTPUT)0;
// Преобразование пространства вида и проекция output.position = mul(input.position, ViewProjMatrix);
// Делаем рассеиваемую составляющую цвета синей output.diffuse = Blue;
// Возвращаем спроецированную и окрашенную вершину return output; }
Получение дескриптора константы
16.2.1.1. Получение дескриптора константы
Чтобы установить значение какой-нибудь переменной шейдера из кода нашего приложения, необходим способ сослаться на эту переменную. Для этой цели применяется тип D3DXHANDLE. Приведенный ниже метод возвращает значение типа D3DXHANDLE, указывающее на переменную шейдера с заданным именем:
D3DXHANDLE ID3DXConstantTable::GetConstantByName( D3DXHANDLE hConstant, // область видимости LPCSTR pName // имя );
hConstant— Значение D3DXHANDLE задающее родительскую структуру запрашиваемой переменной, определяющую время ее жизни. Например, если мы хотим получить дескриптор отдельного члена объявленной в шейдере структуры данных, то здесь нам надо указать дескриптор этой структуры. Если мы получаем дескриптор переменной самого верхнего уровня, в этом параметре передается 0.
pName — указанное в исходном коде шейдера имя переменной, для которой мы получаем дескриптор.
Например, если имя переменной в коде шейдера ViewProjMatrix и это переменная верхнего уровня, то для получения дескриптора следует написать:
// Получение дескриптора переменной шейдера ViewProjMatrix D3DXHANDLE h0; h0 = ConstTable->GetConstantByName(0, "ViewProjMatrix");
Поток выполнения программы
16.4.2. Поток выполнения программы
Набор поддерживаемых HLSL инструкций для ветвления, повторов и общего потока программы очень похож на инструкции C++. Синтаксис этих инструкций тоже полностью аналогичен C++.
Инструкция Return:
return (выражение);
Инструкции If и If...Else:
if(условие) { инструкция(s); }
if( условие ) { инструкция(s); } else { инструкция(s); }
Инструкция for:
for(инициализация; условие; изменение) { инструкция(s); }
Инструкция while:
while( условие ) { инструкция(s); }
Инструкция do...while:
do { инструкция(s); } while(условие);
Префиксы переменных
16.3.7. Префиксы переменных
Приведенные ниже ключевые слова можно использовать в качестве префиксов при объявлении переменных:
static — Если глобальная переменная объявляется с префиксом static, это означает, что переменная не должна быть доступна вне шейдера. Другими словами, она будет локальной для шейдера. Если же с префиксом static объявляется локальная переменная, то она будет вести себя так же, как локальная статическая переменная в С++. Это значит, что она инициализируется один раз при первом выполнении функции, а затем ее значение сохраняется между вызовами функции. Если в объявлении переменной нет инициализации, ей автоматически присваивается значение 0.
static int x = 5;
uniform — Если переменная объявлена с префиксом uniform, это означает, что она инициализируется вне шейдера, например в коде приложения, и передается в шейдер.
extern — Если переменная объявлена с префиксом extern, это значит, что она должна быть доступна вне шейдера, например из кода приложения. Этот префикс можно указывать только для глобальных переменных. Нестатические глобальные переменные будут внешними по умолчанию.
shared — Если переменная объявлена с префиксом shared, это указывает каркасу эффектов (см. главу 19), что переменная совместно используется несколькими эффектами. Префикс shared может использоваться только для глобальных переменных.
volatile — Если переменная объявлена с префиксом volatile, это указывает каркасу эффектов (см. главу 19), что значение переменной будет часто изменяться. Префикс volatile может использоваться только для глобальных переменных.
const — Ключевое слово const в HLSL имеет тот же самый смысл, что и в C++. Значит, если переменная объявлена с префиксом const, то она является константой и ее значение не может меняться.
const float pi = 3.14f;
Приведение типов
16.4.3. Приведение типов
HLSL поддерживает очень гибкую схему приведения типов. Синтаксис приведения в HLSL тот же самый, что и в C. Например, чтобы преобразовать значение типа float в значение типа matrix, мы напишем:
float f = 5.0f; matrix m = (matrix)f;
В примерах из этой книги вы сможете понять смысл приведения из синтаксиса. Однако, если вам потребуется дополнительная информация по поддерживаемым приведениям типов, вы найдете ее в документации DirectX SDK, выбрав на вкладке Contents пункт DirectX Graphics\Reference\Shader Reference\High Level Shading Language\Type.
Скалярные типы
16.3.1. Скалярные типы
HLSL поддерживает следующие скалярные типы:
bool — Логическое значение «истина» или «ложь». Обратите внимание, что в HLSL есть ключевые слова true и false.
int — 32-разрядное целое со знаком.
half — 16-разрядное число с плавающей точкой.
float — 32-разрядное число с плавающей точкой.
double — 64-разрядное число с плавающей точкой.
ПРИМЕЧАНИЕ
Структуры
16.3.5. Структуры
Структуры объявляются точно так же, как это делается в С++. Однако, членами структур в HLSL не могут быть функции. Вот пример объявления структуры в HLSL:
struct MyStruct { matrix T; vector n; float f; int x; bool b; };
MyStruct s; // создаем экземпляр s.f = 5.0f; // доступ к члену
Таблица констант
16.2.1. Таблица констант
В каждом шейдере есть таблица констант, используемая для хранения его переменных. Библиотека D3DX обеспечивает приложению доступ к таблице констант шейдера через интерфейс ID3DXConstantTable. Через этот интерфейс мы можем устанавливать значения переменных шейдера из кода нашего приложения.
Сейчас мы приведем сокращенный список методов, реализуемых интерфейсом ID3DXConstantTable. Если вам необходим полный список, обратитесь к документации Direct3D.
Типы переменных
ПРИМЕЧАНИЕ
Точка входа
16.1.3. Точка входа
Подобно программам на C++, у каждой программы на HLSL есть точка входа. В нашем примере вершинного шейдера мы назвали являющуюся точкой входа функцию Main, но это не обязательно. В качестве точки входа шейдера может использоваться любая функция, независимо от ее имени. Учтите, что у этой функции должны быть входные параметры, которые используются для передачи данных исходной вершины в шейдер. Кроме того, функция должна возвращать выходную структуру, применяемую для возврата вершины, обработанной нашим шейдером.
VS_OUTPUT Main(VS_INPUT input) {
ПРИМЕЧАНИЕ
float4 Main(in float2 base : TEXCOORD0, in float2 spot : TEXCOORD1, in float2 text : TEXCOORD2) : COLOR { ... }
Параметры передаются в шейдер; в данном примере мы передаем шейдеру три набора координат текстуры. Шейдер возвращает единственное значение цвета, на что указывает констркукия : COLOR следующая за сигнатурой функции. Это определение эквивалентно следующему:
struct INPUT { float2 base : TEXCOORD0; float2 spot : TEXCOORD1; float2 text : TEXCOORD2; };
struct OUTPUT { float4 c : COLOR; };
OUTPUT Main(INPUT input) { ... }
Код функции, являющейся входной точкой, отвечает за вычисление данных возвращаемой вершины на основе полученных данных исходной вершины. Рассматриваемый в примере шейдер просто преобразует координаты вершины в пространство вида и пространство проекции, устанавливает для вершины синий цвет и возвращает полученную в результате вершину. Сперва мы создаем экземпляр выходной структуры VS_OUTPUT и присваиваем всем ее членам 0.
VS_OUTPUT output = (VS_OUTPUT)0; // обнуляем все члены
Затем наш шейдер выполняет преобразование координат исходной вершины, умножая ее на переменную ViewProjMatrix с помощью функции mul, которая является встроенной функцией, выполняющей операции умножения вектора на матрицу и умножения матрицы на матрицу. Преобразованный вектор местоположения вершины мы сохраняем в члене position экземпляра выходной структуры данных:
// Преобразование и проекция output.position = mul(input.position, ViewProjMatrix);
Затем мы устанавливаем член данных, задающий рассеиваемую составляющую цвета, равной вектору Blue:
// Делаем рассеиваемую составляющую цвета синей output.diffuse = Blue;
И, наконец, мы возвращаем полученную вершину:
return output; }
Установка констант
16.2.1.2. Установка констант
Как только наше приложение получило значение D3DXHANDLE, ссылающееся на требуемую переменную в коде шейдера, мы можем установить значение этой переменной из нашего приложения с помощью метода ID3DXConstantTable::SetXXX, где XXX заменяется на название типа переменной, значение которой устанавливается. Например, если мы хотим установить значения массива векторов, следует воспользоваться методом SetVectorArray.
Общий синтаксис всех методов ID3DXConstantTable::SetXXX выглядит так:
HRESULT ID3DXConstantTable::SetXXX( LPDIRECT3DDEVICE9 pDevice, D3DXHANDLE hConstant, XXX value );
pDevice — Указатель на устройство с которым связана таблица констант.
hConstant — Дескриптор, ссылающийся на переменную, значение которой мы устанавливаем.
value — Присваиваемое переменной значение, где XXX заменяется на название типа переменной, значение которой мы устанавливаем. Для некоторых значений (bool, int, float) мы передаем само значение, а для других (векторы, матрицы, структуры) — ссылку на значение.
Если мы инициализируем массив, то у метода SetXXX появляется дополнительный четвертый параметр, задающий количество элементов массива. Например, прототип метода для установки значений массива четырехмерных векторов, выглядит так:
HRESULT ID3DXConstantTable::SetVectorArray( LPDIRECT3DDEVICE9 pDevice, // связанное устройство D3DXHANDLE hConstant, // дескриптор переменной шейдера CONST D3DXVECTOR4* pVector, // указатель на массив UINT Count // количество элементов массива );
Приведенный ниже список описывает типы, которые мы можем инициализировать с помощью интерфейса ID3DXConstantTable. Подразумевается, что корректное устройство (Device) и корректный дескриптор переменной (handle) уже получены.
SetBool — используется для установки логических значений. Пример вызова:
bool b = true; ConstTable->SetBool(Device, handle, b);
SetBoolArray — Используется для установки массива логических значений.
Пример вызова:
bool b[3] = {true, false, true}; ConstTable->SetBoolArray(Device, handle, b, 3);
SetFloat — Используется для установки значения с плавающей точкой. Пример вызова:
float f = 3.14f; ConstTable->SetFloat(Device, handle, f);
SetFloatArray — Используется для установки массива значений с плавающей точкой. Пример вызова:
float f[2] = {1.0f, 2.0f}; ConstTable->SetFloatArray(Device, handle, f, 2);
SetInt — Используется для установки целочисленного значения. Пример вызова:
int x = 4; ConstTable->SetInt(Device, handle, x);
SetIntArray — Используется для установки массива целых чисел. Пример вызова:
int x[4] = {1, 2, 3, 4}; ConstTable->SetIntArray(Device, handle, x, 4);
SetMatrix — Используется для установки матрицы 4 × 4. Пример вызова:
D3DXMATRIX M(...); ConstTable->SetMatrix(Device, handle, &M);
SetMatrixArray — Используется для установки массива матриц 4 × 4. Пример вызова:
D3DXMATRIX M[4];
// ...Инициализация матриц
ConstTable->SetMatrixArray(Device, handle, M, 4);
SetMatrixPointerArray — Используется для установки массива указателей на матрицы 4 × 4. Пример вызова:
D3DXMATRIX* M[4];
// ...Выделение памяти и инициализация указателей
ConstTable->SetMatrixPointerArray(Device, handle, M, 4);
SetMatrixTranspose — используется для установки транспонированной матрицы 4 × 4. Пример вызова:
D3DXMATRIX M(...); D3DXMatrixTranspose(&M, &M); ConstTable->SetMatrixTranspose(Device, handle, &M);
SetMatrixTransposeArray — Используется для установки массива транспонированных матриц 4 × 4. Пример вызова:
D3DXMATRIX M[4];
// ...Инициализация матриц и их транспонирование
ConstTable->SetMatrixTransposeArray(Device, handle, M, 4);
SetMatrixTransposePointerArray — Используется для установки массива указателей на транспонированные матрицы 4 × 4.Пример вызова:
D3DXMATRIX* M[4];
// ...Выделение памяти, инициализация указателей и транспонирование
ConstTable->SetMatrixTransposePointerArray(Device, handle, M, 4);
SetVector — Используется для установки переменной типа D3DXVECTOR4. Пример вызова:
D3DXVECTOR4 v(1.0f, 2.0f, 3.0f, 4.0f); ConstTable->SetVector(Device, handle, &v);
SetVectorArray — Используется для установки массива векторов. Пример вызова:
D3DXVECTOR4 v[3];
// ...Инициализация векторов
ConstTable->SetVectorArray(Device, handle, v, 3);
SetValue — Используется для установки значения произвольного размера, например, структуры. В приведенном примере мы используем SetValue для установки значений D3DXMATRIX:
D3DXMATRIX M(...); ConstTable->SetValue(Device, handle, (void*)&M, sizeof(M));
Установка значений по умолчанию для констант
16.2.1.3. Установка значений по умолчанию для констант
Приведенный ниже метод присваивает всем константам значения по умолчанию, то есть те значения, которые были заданы при объявлении переменных. Метод должен вызываться один раз при инициализации данных приложения.
HRESULT ID3DXConstantTable::SetDefaults( LPDIRECT3DDEVICE9 pDevice );
pDevice— Указатель на связанное с таблицей констант устройство.
Векторные типы
16.3.2. Векторные типы
HLSL поддерживает следующие встроенные векторные типы:
vector— Четырехмерный вектор, каждая компонента которого имеет тип float.
vector<T, n> — n-мерный вектор, каждая компонента которого относится к скалярному типу T. Размерность n может быть от 1 до 4. Вот пример двухмерного вектора с компонентами типа double:
vector<double, 2> vec2;
Доступ к отдельным компонентам вектора осуществляется с использованием синтаксиса доступа к элементу массива по индексу. Например, чтобы установить значение i-ой компоненты вектора vec, следует написать:
vec[i] = 2.0f;
Кроме того, мы можем обращаться к компонентам вектора vec как к членам структуры, используя предопределенные имена компонентов x, y, z, w, r, g, b и a.
vec.x = vec.r = 1.0f; vec.y = vec.g = 2.0f; vec.z = vec.b = 3.0f; vec.w = vec.a = 4.0f;
Имена r, g, b и a ссылаются на те же самые компоненты, что и имена x, y, z и w, соответственно. Когда вектор используется для представления цвета, нотация RGBA более предпочтительна, поскольку подчеркивает тот факт, что вектор содержит цветовые значения, а не координаты.
Помимо этого, мы можем пользоваться следующими предопределенными типами для представления двухмерных, трехмерных и четырехмерных векторов соответственно:
float2 vec2; float3 vec3; float4 vec4;
Возьмем вектор u = (ux, uy, uz, uw) и предположим, что мы хотим скопировать компоненты вектора u в вектор v, чтобы получить v = (ux, uy, uy, uw). Первое, что приходит на ум, скопировать каждую компоненту u в соответствующую компоненту v. Однако, HLSL предоставляет специальный синтаксис для таких операций копирования с изменением последовательности, называемый перенос по адресам (swizzles):
vector u = {1.0f, 2.0f, 3.0f, 4.0f}; vector v = {0.0f, 0.0f, 5.0f, 6.0f}; v = u.xyyw; // v = {1.0f, 2.0f, 2.0f, 4.0f}
При копировании векторов мы не обязаны копировать все их компоненты. Например, мы можем скопировать только компоненты x и y, как показано в приведенном ниже фрагменте кода:
vector u = {1.0f, 2.0f, 3.0f, 4.0f}; vector v = {0.0f, 0.0f, 5.0f, 6.0f}; v.xy = u; // v = {1.0f, 2.0f, 5.0f, 6.0f}
Входная и выходная структуры
16.1.2. Входная и выходная структуры
За объявлением глобальных переменных следует объявление двух специальных структур, которые мы будем называть входной (input) и выходной (output) структурами. Для вершинных шейдеров эти структуры описывают данные вершины, которые соответственно, получает и возвращает наш шейдер.
struct VS_INPUT { vector position : POSITION; };
struct VS_OUTPUT { vector position : POSITION; vector diffuse : COLOR; };
ПРИМЕЧАНИЕ
В рассматриваемом примере вершина, поступающая на вход шейдера содержит только данные о местоположении. Возвращаемая нашим шейдером вершина содержит сведения о местоположении а также данные цвета.
Синтаксическая конструкция с двоеточием применяется для указания способа использования переменной. Это похоже на поле настраиваемого формата вершин (FVF) в структуре данных вершины. Например, во входной структуре данных VS_INPUT, у нас есть член
vector position : POSITION;
Конструкция :POSITION означает, что переменная position типа vector используется для описания местоположения передаваемой шейдеру вершины. В качестве другого примера можно рассмотреть описание члена структуры VS_OUTPUT:
vector diffuse : COLOR;
Здесь : COLOR означает, что переменная diffuse типа vector применяется для описания цвета возвращаемой шейдером вершины. О доступных идентификаторах способов использования переменных, применяемых для вершинных и пиксельных шейдеров, мы поговорим в следующих двух главах.
ПРИМЕЧАНИЕ
Встроенные функции
В HLSL есть богатый набор встроенных функций, часто используемых для трехмерной графики. Ниже приведена таблица с сокращенным списком этих функций. В следующих двух главах мы попрактикуемся в применении некоторых из них. А сейчас давайте просто познакомимся с этими функциями.
ПРИМЕЧАНИЕ
Функция | Описание |
abs(x) | Возвращает |x|. |
ceil(x) | Возвращает наименьшее целое, которое больше или равно x. |
clamp(x, a, b) | Ограничивает x в диапазоне [a, b] и возвращает результат. Если x меньше a возвращается a, если x больше b, возвращается b, в остальных случаях возвращается x. |
cos(x) | Возвращает косинус x, x — в радианах. |
cross(u, v) | Возвращает u × v. |
degrees(x) | Преобразует x из радиан в градусы. |
determinant(M) | Возвращает детерминант матрицы det(M). |
distance(u, v) | Возвращает расстояние |v – u| между точкам u и v. |
dot(u, v) | Возвращает u Ч v. |
floor(x) | Возвращает наибольшее целое, которое меньше или равно x. |
length(v) | Возвращает |v|. |
lerp(u, v, t) | Линейная интерполяция между u и v на основании параметра t, находящегося в диапазоне [0, 1]. |
log(x) | Возвращает ln(x). |
log10(x) | Возвращает log10(x). |
log2(x) | Возвращает log2(x). |
max(x, y) | Возвращает x если x ≥ y, иначе возвращает y. |
min(x, y) | Возвращает x если x ≤ y, иначе возвращает y. |
mul(M, N) | Возвращает произведение матриц MN. Обратите внимание, что произведение матриц MN должно быть определено. Если M это вектор, он используется как вектор-строка, чтобы было определено произведение вектора на матрицу. Точно так же, если N это вектор, то он используется как вектор-столбец, чтобы было определено произведение матрицы на вектор. |
normalize(v) | Возвращает v/|v|. |
pow(b, n) | Возвращает bn. |
radians(x) | Преобразует x из градусов в радианы. |
reflect(v, n) | Вычисляет вектор отражения по исходному вектору v и нормали поверхности n. |
refract(v, n, eta) | Вычисляет вектор преломления по исходному вектору v, нормали поверхности n и отношению коэффициентов преломления двух материалов eta. Чтобы получить дополнительные сведения о преломлении, посмотрите закон Снеллиуса в учебнике физики. |
rsqrt(x) | Возвращает 1/√x. |
saturate(x) | Возвращает clamp(x, 0.0, 1.0). |
sin(x) | Возвращает синус x, x в радианах. |
sincos(in x, out s, out c) | Возвращает синус и косинус x, x в радианах. |
sqrt(x) | Возвращает √x. |
tan(x) | Возвращает тангенс x, x в радианах. |
transpose(M) | Возвращает транспонированную матрицу MT. |
ПРИМЕЧАНИЕ
float3 v = float3(0.0f, 0.0f, 0.0f); v = cos(v);
то функция будет применена к каждой компоненте : v = (cos(x), cos(y), cos(z)).
float x = sin(1.0f); // синус 1.0f радиан
float y = sqrt(4.0f); // квадратный корень из 4.
vector u = {1.0f, 2.0f, -3.0f, 0.0f}; vector v = {3.0f, -1.0f, 0.0f, 2.0f}; float s = dot(u, v); // скалярное произведение векторов u и v.
float3 i = {1.0f, 0.0f, 0.0f}; float3 j = {0.0f, 1.0f, 0.0f}; float3 k = cross(i, j); // векторное произведение векторов i и j.
matrix<float, 2, 2> M = {1.0f, 2.0f, 3.0f, 4.0f}; matrix<float, 2, 2> T = transpose(M); // транспонирование матрицы