Поделиться через


Работа с шейдерами и ресурсами шейдеров

Пришло время узнать, как работать с шейдерами и ресурсами шейдеров при разработке игры 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).
  • s*#*: регистрация для примера. (Пример определяет поведение подстановки для текселей в ресурсе текстуры.)

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

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

Чтобы назначить буферы констант регистрам— при настройке конвейера необходимо подключить буфер константы к тому же слоту, которому он назначен в файле HLSL. Например, в предыдущем разделе вызов VSSetConstantBuffers указывает "0" для первого параметра. Это указывает Direct3D подключить ресурс буфера константы для регистрации 0, который соответствует назначению буфера для регистрации (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 имеет данные, необходимые для выполнения следующего шага рисования. Обратите внимание, что между выходными данными вершинного шейдера и входным шейдером пикселей не существует; вершинный шейдер возвращает одну структуру для каждой вершины, которая она задана, но шейдер пикселей выполняется один раз для каждого пикселя. Это связано с тем, что данные по вершинам сначала проходят через этап растеризации. На этом этапе определяется, какие пиксели "охватывают" геометрию, вычисляет интерполированные данные вершин для каждого пикселя, а затем вызывает шейдер пикселей один раз для каждого из этих пикселей. Интерполяция — это поведение по умолчанию при растеризации выходных значений и важно, в частности, для правильной обработки выходных векторных данных (векторов света, норм для вершин и тангенсов и других).

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;
}

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

// 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 не будет отображаться!

Пример более сложного шейдера пикселей для выполнения заливки Phong может выглядеть следующим образом. Так как векторы и нормализации были интерполированы, нам не нужно вычислять их на основе пикселей. Однако мы должны повторно нормализовать их из-за того, как работает интерполяция; концептуально мы должны постепенно "спинировать" вектор от направления в вершине А к направлению на вершине B, сохраняя ее длину — интерполяция wheras вместо того, чтобы сократить по прямой линии между двумя векторными конечными точками.

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 и s, соответственно), вы также можете получить к ним доступ в шейдере пикселей.

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 11at этот момент и готовы начать работу над проектом. Ниже приведены некоторые ссылки, которые помогут ответить на другие вопросы о разработке с помощью DirectX и C++:

Работа с ресурсами устройства DirectX

Общие сведения о конвейере отрисовки Direct3D 11