Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Пришло время узнать, как работать с шейдерами и ресурсами шейдеров при разработке игры Microsoft DirectX для Windows 8. Мы видели, как настроить графическое устройство и ресурсы, и, возможно, вы даже начали изменять его поток выполнения. Итак, давайте рассмотрим пиксельные и вершинные шейдеры.
Если вы не знакомы с языками шейдеров, стоит провести краткое обсуждение. Шейдеры — это небольшие низкоуровневые программы, которые компилируются и выполняются на определенных этапах графического конвейера. Их специализация — это очень быстрые математические операции с плавающей запятой. Наиболее распространенными программами шейдера являются:
- шейдер вершин— выполняется для каждой вершины сцены. Этот шейдер работает с элементами буфера вершин, предоставленными ему вызывающим приложением, и минимально приводит к 4-компонентному вектору положения, который будет растеризован в положение пикселя.
- шейдер пикселей— выполняется для каждого пикселя в целевом объекте отрисовки. Этот шейдер получает растровые координаты из предыдущих этапов шейдера (в самых простых конвейерах, это будет шейдер вершин) и возвращает цвет (или другое 4-компонентное значение) для этой позиции пикселя, которая затем записывается в целевой объект отрисовки.
В этом примере используются очень простые вершинные и пиксельные шейдеры, которые только рисуют геометрию, и более сложные шейдеры, выполняющие базовые вычисления освещения.
Программы шейдера написаны на языке шейдеров высокого уровня Майкрософт (HLSL). Синтаксис HLSL выглядит так же, как C, но без указателей. Программы шейдера должны быть очень компактными и эффективными. Если при компиляции шейдера получается слишком много инструкций, его невозможно запустить, и возвращается ошибка. (Обратите внимание, что точное количество инструкций, разрешённых является частью уровня возможностей Direct3D .)
В Direct3D шейдеры не компилируются во время выполнения; Они компилируются при компиляции остальной части программы. При компиляции приложения с использованием Microsoft Visual Studio 2013 файлы HLSL компилируются в файлы CSO (.cso), которые ваше приложение должно загружать и размещать в памяти GPU перед их отображением. Убедитесь, что эти файлы CSO включены в приложение при его упаковке; они являются ресурсами так же, как сетки и текстуры.
Общие сведения о семантике HLSL
Прежде чем продолжить, важно обсудить семантику HLSL, так как они часто являются точкой путаницы для новых разработчиков Direct3D. Семантика HLSL — это строки, определяющие значение, переданное между приложением и программой шейдера. Хотя они могут быть любой из различных возможных строк, рекомендуется использовать строку, например POSITION
или COLOR
, которая указывает на использование. Эти семантики назначаются при создании буфера констант или макета входных данных. Можно также добавить число от 0 до 7 к семантике, чтобы использовать отдельные регистры для аналогичных значений. Например: COLOR0, COLOR1, COLOR2...
Семантики, которые начинаются с "SV_", являются системными значениями, записываемыми программой шейдера, и ваша игра (выполняемая на ЦП) не может их изменять. Как правило, эти семантики содержат значения, которые являются входными или выходными данными из другого этапа шейдера в графическом конвейере или создаются полностью GPU.
Кроме того, SV_
семантики имеют разные характеристики, если они используются для указания входных данных или выходных данных из этапа шейдера. Например, SV_POSITION
(данные на выходе) содержат данные вершин, преобразованные на стадии шейдера вершин, и SV_POSITION
(входные) содержит значения положения пикселей, которые были интерполированы GPU во время стадии растеризации.
Ниже приведены некоторые распространенные семантики HLSL:
-
POSITION
(n) для данных буфера вершин.SV_POSITION
предоставляет позицию пикселя для шейдера пикселей и не может быть написана вашей игрой. -
NORMAL
(n) для обычных данных, предоставляемых буфером вершин. -
TEXCOORD
(n) для данных координат УФ текстуры, предоставленных шейдеру. -
COLOR
(n) для данных цвета RGBA, предоставленных шейдеру. Обратите внимание, что она обрабатывается одинаково с данными координат, включая интерполяцию значения во время растеризации; Семантика просто помогает определить, что это цветовые данные. -
SV_Target
[n] для записи из пиксельного шейдера в целевую текстуру или другой буфер пикселей.
Мы увидим некоторые примеры семантики HLSL, как мы рассмотрим пример.
Чтение из буферов констант
Любой шейдер может считывать из константного буфера, если этот буфер присоединен к его этапу в качестве ресурса. В этом примере только шейдеру вершин назначается буфер констант.
Буфер константы объявляется в двух местах: в коде C++ и в соответствующих файлах HLSL, к которым он получит доступ.
Вот как структуру буфера констант объявляется в коде C++.
typedef struct _constantBufferStruct {
DirectX::XMFLOAT4X4 world;
DirectX::XMFLOAT4X4 view;
DirectX::XMFLOAT4X4 projection;
} ConstantBufferStruct;
При объявлении структуры для буфера констант в коде C++ убедитесь, что все данные правильно выровнены вдоль 16-байтовых границ. Самый простой способ сделать это — использовать типы DirectXMath, например, XMFLOAT4 или XMFLOAT4X4, как показано в примере кода. Кроме того, вы можете защититься от неправильных буферов, объявив статическое утверждение:
// Assert that the constant buffer remains 16-byte aligned.
static_assert((sizeof(ConstantBufferStruct) % 16) == 0, "Constant Buffer size must be 16-byte aligned");
Эта строка кода приведет к ошибке во время компиляции, если ConstantBufferStruct не соответствует 16 байтам. Для получения дополнительной информации о выравнивании и упаковке буферов постоянных значений см. Правила упаковки для константных переменных.
Вот как объявляется буфер константы в шейдере вершин HLSL.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
matrix mWorld; // world matrix for object
matrix View; // view matrix
matrix Projection; // projection matrix
};
Все буферы — константа, текстура, образец или другое — должны иметь регистр, чтобы графический процессор смог получить к ним доступ. Каждый этап шейдера позволяет до 15 буферов констант, и каждый буфер может содержать до 4096 переменных констант. Синтаксис объявления использования регистров следующий:
- b*#*: регистр для буфера констант (cbuffer).
- t*#*: регистр для буфера текстуры (tbuffer).
- *#*: регистр для сэмплера. (Семплер определяет поведение интерполяции текселей в ресурсе текстуры.)
Например, HLSL для шейдера пикселей может принимать текстуру и сэмплер в качестве входных данных с таким объявлением, как это.
Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);
Чтобы назначить буферы констант регистрам — при настройке конвейера необходимо подключить буфер констант к тому же слоту, которому он назначен в файле HLSL. Например, в предыдущем разделе вызов VSSetConstantBuffers указывает "0" для первого параметра. Это указывает Direct3D подключить ресурс буфера констант к регистру 0, что соответствует присвоению буфера к регистру register(b0) в файле HLSL.
Чтение из буферов вершин
Буфер вершин передает данные треугольников объектам сцены шейдерам вершин. Как и в случае с буфером констант, структуру буфера вершин объявляется в коде C++ с использованием аналогичных правил упаковки.
typedef struct _vertexPositionColor
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 color;
} VertexPositionColor;
Стандартный формат данных вершин в Direct3D 11 отсутствует. Вместо этого мы определяем макет данных вершин с помощью дескриптора; поля данных задаются массивом структур D3D11_INPUT_ELEMENT_DESC. Здесь показан простой макет входных данных, описывающий тот же формат вершин, что и предыдущая структура:
D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
hr = device->CreateInputLayout(
iaDesc,
ARRAYSIZE(iaDesc),
bytes,
bytesRead,
&m_pInputLayout
);
Если вы добавляете данные в формат вершин при изменении примера кода, обязательно обновите входной макет, или шейдер не сможет интерпретировать его. Вы можете изменить макет вершин, как показано ниже.
typedef struct _vertexPositionColorTangent
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT3 tangent;
} VertexPositionColorTangent;
В этом случае вы измените определение макета ввода следующим образом.
D3D11_INPUT_ELEMENT_DESC iaDescExtended[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
hr = device->CreateInputLayout(
iaDesc,
ARRAYSIZE(iaDesc),
bytes,
bytesRead,
&m_pInputLayoutExtended
);
Каждое из определений элементов входного макета префиксируется строкой, например POSITION или NORMAL, то есть семантикой, которую мы обсуждали ранее в этом разделе. Это как дескриптор, который помогает GPU определить соответствующий элемент при обработке вершины. Выберите общие, значимые имена для элементов вершины.
Как и в случае с буфером констант, шейдер вершин имеет соответствующее определение буфера для входящих элементов вершин. (Именно поэтому при создании входной вершинной структуры мы предоставили ссылку на ресурс вершинного шейдера — Direct3D проверяет макет данных каждой вершины с помощью структуры входных данных шейдера.) Обратите внимание, как семантика совпадает между определением входной структуры и объявлением буфера HLSL. К COLOR
добавлен "0". Не обязательно добавлять 0, если в макете объявлен только один элемент COLOR
, однако рекомендуется сделать это на случай, если вы решите добавить больше цветовых элементов в будущем.
struct VS_INPUT
{
float3 vPos : POSITION;
float3 vColor : COLOR0;
};
Передача данных между шейдерами
Шейдеры принимают входные типы и возвращают типы выходных данных в своих основных функциях во время выполнения. Для шейдера вершин, определенного в предыдущем разделе, входной тип был структурой VS_INPUT, и мы определили соответствующий макет и структуру C++. Массив этой структуры используется для создания буфера вершин в методе CreateCube.
Шейдер вершин возвращает структуру PS_INPUT, которая должна как минимум содержать 4-компонентную (float4) окончательную позицию вершины. Это значение позиции должно иметь объявленную для него семантику системного значения SV_POSITION
, чтобы GPU имел данные, необходимые для выполнения следующего этапа рисования. Обратите внимание, что между выходными данными вершинного шейдера и входными данными пиксельного шейдера нет соответствия 1:1; вершинный шейдер возвращает одну структуру для каждой вершины, которой он задан, но пиксельный шейдер выполняется один раз для каждого пикселя. Это связано с тем, что данные по вершинам сначала проходят через этап растеризации. На этом этапе определяется, какие пиксели "охватывают" геометрию, вычисляются интерполированные данные по вершинам для каждого пикселя, а затем шейдер пикселей вызывается один раз для каждого из этих пикселей. Интерполяция — это поведение по умолчанию при растеризации выходных значений и важно, в частности, для правильной обработки выходных векторных данных (векторов света, норм для вершин и тангенсов и других).
struct PS_INPUT
{
float4 Position : SV_POSITION; // interpolated vertex position (system value)
float4 Color : COLOR0; // interpolated diffuse color
};
Проверка шейдера вершин
Пример шейдера вершин очень прост: принимает вершину (положение и цвет), преобразует позицию из координат модели в проецируемые координаты и возвращает его (вместе с цветом) растризатору. Обратите внимание, что значение цвета интерполируется прямо вместе с данными положения, предоставляя другое значение для каждого пикселя, даже если вершинный шейдер не выполнял никаких вычислений по значению цвета.
VS_OUTPUT main(VS_INPUT input) // main is the default function name
{
VS_OUTPUT Output;
float4 pos = float4(input.vPos, 1.0f);
// Transform the position from object space to homogeneous projection space
pos = mul(pos, mWorld);
pos = mul(pos, View);
pos = mul(pos, Projection);
Output.Position = pos;
// Just pass through the color data
Output.Color = float4(input.vColor, 1.0f);
return Output;
}
Более сложный шейдер вершин, например такой, который настраивает вершины объекта для шейдинга по Фонгу, может выглядеть примерно так. В этом случае мы используем то, что векторы и нормали интерполируются, чтобы приблизить видимость гладкой поверхности.
// A constant buffer that stores the three basic column-major matrices for composing geometry.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
matrix model;
matrix view;
matrix projection;
};
cbuffer LightConstantBuffer : register(b1)
{
float4 lightPos;
};
struct VertexShaderInput
{
float3 pos : POSITION;
float3 normal : NORMAL;
};
// Per-pixel color data passed through the pixel shader.
struct PixelShaderInput
{
float4 position : SV_POSITION;
float3 outVec : POSITION0;
float3 outNormal : NORMAL0;
float3 outLightVec : POSITION1;
};
PixelShaderInput main(VertexShaderInput input)
{
// Inefficient -- doing this only for instruction. Normally, you would
// premultiply them on the CPU and place them in the cbuffer.
matrix mvMatrix = mul(model, view);
matrix mvpMatrix = mul(mvMatrix, projection);
PixelShaderInput output;
float4 pos = float4(input.pos, 1.0f);
float4 normal = float4(input.normal, 1.0f);
float4 light = float4(lightPos.xyz, 1.0f);
//
float4 eye = float4(0.0f, 0.0f, -2.0f, 1.0f);
// Transform the vertex position into projected space.
output.gl_Position = mul(pos, mvpMatrix);
output.outNormal = mul(normal, mvMatrix).xyz;
output.outVec = -(eye - mul(pos, mvMatrix)).xyz;
output.outLightVec = mul(light, mvMatrix).xyz;
return output;
}
Обзор шейдера пикселей
Этот шейдер пикселей в этом примере вполне может быть абсолютным минимальным объемом кода, который можно использовать в шейдере пикселей. Он принимает интерполированные данные цвета пикселей, созданные во время растеризации, и возвращает его в качестве выходных данных, где он будет записан в целевой объект отрисовки. Как скучно!
PS_OUTPUT main(PS_INPUT In)
{
PS_OUTPUT Output;
Output.RGBColor = In.Color;
return Output;
}
Важной частью является семантика системного значения SV_TARGET
для возвращаемого значения. Он указывает, что выходные данные записываются в основной целевой объект отрисовки, который является буфером текстуры, предоставленным цепочке буферов для отображения. Это необходимо для шейдеров пикселей - без цветовых данных от шейдера пикселей Direct3D не смог бы ничего отображать!
Пример более сложного пиксельного шейдера для выполнения шейдинга Фонга может выглядеть следующим образом. Так как векторы и нормали были интерполированы, нам не нужно вычислять их для каждого пикселя. Однако мы должны повторно нормализовать их из-за того, как работает интерполяция; концептуально мы должны постепенно "вращать" вектор от направления в вершине А к направлению в вершине B, сохраняя его длину — в то время как интерполяция вместо этого проходит по прямой линии между конечными точками векторов.
cbuffer MaterialConstantBuffer : register(b2)
{
float4 lightColor;
float4 Ka;
float4 Kd;
float4 Ks;
float4 shininess;
};
struct PixelShaderInput
{
float4 position : SV_POSITION;
float3 outVec : POSITION0;
float3 normal : NORMAL0;
float3 light : POSITION1;
};
float4 main(PixelShaderInput input) : SV_TARGET
{
float3 L = normalize(input.light);
float3 V = normalize(input.outVec);
float3 R = normalize(reflect(L, input.normal));
float4 diffuse = Ka + (lightColor * Kd * max(dot(input.normal, L), 0.0f));
diffuse = saturate(diffuse);
float4 specular = Ks * pow(max(dot(R, V), 0.0f), shininess.x - 50.0f);
specular = saturate(specular);
float4 finalColor = diffuse + specular;
return finalColor;
}
В другом примере шейдер пикселей принимает собственные буферы констант, содержащие световые и материальные сведения. Макет входных данных в вершинном шейдере будет расширен, чтобы включить обычные данные, а выходные данные из этого шейдера вершин, как ожидается, включают преобразованные векторы вершины, свет и нормальное вершины в системе координат представления.
Если у вас есть буферы текстур и образцы с назначенными регистрами (t и соответственно), вы также можете получить к ним доступ в шейдере пикселей.
Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float3 norm : NORMAL;
float2 tex : TEXCOORD0;
};
float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
float3 lightDirection = normalize(float3(1, -1, 0));
float4 texelColor = simpleTexture.Sample(simpleSampler, input.tex);
float lightMagnitude = 0.8f * saturate(dot(input.norm, -lightDirection)) + 0.2f;
return texelColor * lightMagnitude;
}
Шейдеры — это очень мощные инструменты, которые можно использовать для создания процедурных ресурсов, таких как карты тени или текстуры шума. На самом деле, расширенные методы требуют, чтобы вы думали о текстурах более абстрактно, а не как визуальные элементы, а как буферы. Они содержат такие данные, как информация о высоте, или другие данные, которые можно использовать на последнем этапе рендеринга в пиксельном шейдере или в этом конкретном кадре в рамках процесса с многослойными эффектами. Многофакторная выборка — это мощный инструмент и основа многих современных визуальных эффектов.
Дальнейшие действия
Надеюсь, вы комфортно себя чувствуете с DirectX 11 на данный момент и готовы приступить к работе над своим проектом. Ниже приведены некоторые ссылки, которые помогут ответить на другие вопросы о разработке с помощью DirectX и C++:
- Разработка игр
- Использование средств Visual Studio для программирования игр DirectX
- разработка игр DirectX и примеры пошаговых руководств
- Дополнительные ресурсы программирования игр
Связанные разделы