Знакомство с пиксельными шейдерами
Пиксельный шейдер (pixel shader) — это программа, выполняемая процессором видеокарты во время процесса растеризации для каждого пикселя. (В отличие от вершинных шейдеров, Direct3D не может программно эмулировать пиксельные шейдеры.) Пиксельные шейдеры заменяют этап мультитекстурирования в фиксированном конвейере функций и предоставляют нам возможность непосредственно управлять отдельными пикселями и получить доступ к текстурным координатам для каждого пикселя. Прямой доступ к пикселям и координатам текстуры позволяет нам реализовать множество специальных эффектов, таких как мультитекстурирование, попиксельное освещение, глубина резкости, моделирование облаков, имитация огня и сложные способы затенения.
Вы можете проверить, какую версию пиксельных шейдеров использует установленная видеокарта, с помощью члена PixelShaderVersion структуры D3DCAPS9 и макроса D3DPS_VERSION. Эта проверка показана в приведенном ниже фрагменте кода:
// Если поддерживаемая устройством версия меньше 2.0 if(caps.PixelShaderVersion < D3DPS_VERSION(2, 0)) // Значит пиксельные шейдеры версии 2.0 устройство не поддерживает
Цели | |
Получить базовое представление о концепции мультитекстурирования. Узнать как написать, создать и использовать пиксельный шейдер. Узнать реализовать мультитекстурирование с помощью пиксельного шейдера. |
Пиксельные шейдеры заменяют этап мультитекстурирования
Пиксельные шейдеры заменяют этап мультитекстурирования в фиксированном конвейере функций. Более того, пиксельные шейдеры предоставляют возможность изменять цвет каждого отдельного пикселя любым способом и предоставляют доступ к данным текстуры, что позворяет реализовать множество специальных эффектов, которые недоступны при работе с фиксированным конвейером функций.Мультитекстурирование — это процесс одновременного наложения нескольких текстур и их смешивания для получения желаемого результата. Мультитекстурирование обычно используется для реализации всего механизма освещения статических объектов.
Встроенный в HLSL объект sampler идентифицирует конкретный этап текстурирования/выборки. Объект sampler используется для ссылки на этап текстурирования/выборки из пиксельного шейдера.
ПРИМЕЧАНИЕ
Этапы работы с пиксельным шейдером
Приведенный ниже список перечисляет действия, которые необходимо выполнить для создания и использования пиксельного шейдера.
Написать и скомпилировать пиксельный шейдер.
Создать представляющий пиксельный шейдер интерфейс IDirect3DPixelShader9 на основании скомпилированного кода шейдера.
Установить пиксельный шейдер с помощью метода IDirect3DDevice9::SetPixelShader.
Конечно же, после завершения работы нам надо уничтожить пиксельный шейдер. В следующих разделах детально рассмотрены все эти этапы.
Комбинирование текстур Пусть b
Рисунок 18.2. Комбинирование текстур. Пусть b, s и t — это цвета соответствующих текселей из текстуры ящика, текстуры прожектора и текстуры текста соответственно. Тогда цвет их комбинации определяется по формуле c = b
Данный пример может быть реализован и без пиксельных шейдеров. Однако, это более простой и прямолинейный способ реализации, позволяющий к тому же продемонстрировать написание, создание и использование пиксельных шейдеров без отвлечения на реализацию алгоритма какого-нибудь специального эффекта.
Хотя в рассматриваемом примере мы используем одновременно только три текстуры, весьма полезно узнать сколько объектов выборки могут одновремено использоваться в каждой из версий пиксельных шейдеров. Другими словами, как количество одновременно используемых текстур зависит от используемой версии пиксельных шейдеров.
Пиксельные шейдеры версий от ps_1_1 до ps_1_3 поддерживают до четырех выборок текстуры.
Пиксельные шейдеры версии ps_1_4 поддерживают до шести выборок тектсуры.
Пиксельные шейдеры версий от ps_2_0 до ps_3_0 поддерживают до 16 выборок текстуры.
Код пиксельного шейдера для реализации мультитекстурирования с использованием трех текстур выглядит следующим образом:
// // Файл : ps_multitex.txt // Описание: Пиксельный шейдер, выполняющий мультитекстурирование //
// // Глобальные переменные //
sampler BaseTex; sampler SpotLightTex; sampler StringTex;
// // Структуры //
struct PS_INPUT { float2 base : TEXCOORD0; float2 spotlight : TEXCOORD1; float2 text : TEXCOORD2; };
struct PS_OUTPUT { vector diffuse : COLOR0; };
// // Точка входа //
PS_OUTPUT Main(PS_INPUT input) { // Обнуляем члены выходной структуры PS_OUTPUT output = (PS_OUTPUT)0;
// Выборка данных из соответствующих текстур vector b = tex2D(BaseTex, input.base); vector s = tex2D(SpotLightTex, input.spotlight); vector t = tex2D(StringTex, input.text);
// Комбинирование цветов текселей vector c = b * s + t;
// Слегка увеличиваем яркость пикселя c += 0.1f;
// Сохраняем результатирующий цвет output.diffuse = c;
return output; }
Сперва в пиксельном шейдере мы объявляем три объекта выборки — по одному для каждой, участвующей в смешивании текстуры. Затем мы описываем входную и выходную структуры. Обратите внимание, что в пиксельный шейдер не передается никаких значений цветов; это вызвано тем, что для текстурирования и освещения объекта применяются только текстуры. Так, BaseTex хранит цвета нашей поверхности, а SpotLightTex — карту освещения. Пиксельный шейдер возвращает единственное значение цвета, которое определяет вычисленный нами цвет данного пикселя.
Функция Main выполняет выборку значений для трех текстур с помощью функции tex2D. Таким образом, мы получаем отображаемые на обрабатываемый в данный момент пиксель тексели каждой из текстур, определяемые на основе заданных координат текстуры и объекта выборки. Затем мы комбинируем цвета текселей согласно формуле c = b * s + t. После этого мы слегка осветляем полученный пиксель, добавляя 0.1f к каждой из его компонент. И, наконец, мы сохраняем цвет полученного в результате пикселя и взвращаем его.
Теперь, посмотрев на код пиксельного шейдера, мы готовы переключить передачу и отправиться к коду приложения. К рассматриваемой нами теме относятся следующие глобальные переменные приложения:
IDirect3DPixelShader9* MultiTexPS = 0; ID3DXConstantTable* MultiTexCT = 0;
IDirect3DVertexBuffer9* QuadVB = 0;
IDirect3DTexture9* BaseTex = 0; IDirect3DTexture9* SpotLightTex = 0; IDirect3DTexture9* StringTex = 0;
D3DXHANDLE BaseTexHandle = 0; D3DXHANDLE SpotLightTexHandle = 0; D3DXHANDLE StringTexHandle = 0;
D3DXCONSTANT_DESC BaseTexDesc; D3DXCONSTANT_DESC SpotLightTexDesc; D3DXCONSTANT_DESC StringTexDesc;
Структура данных вершины для примера мультитекстурирования выглядит следующим образом:
struct MultiTexVertex { MultiTexVertex(float x, float y, float z, float u0, float v0, float u1, float v1, float u2, float v2) { _x = x; _y = y; _z = z; _u0 = u0; _v0 = v0; _u1 = u1; _v1 = v1; _u2 = u2, _v2 = v2; }
float _x, _y, _z; float _u0, _v0; float _u1, _v1; float _u2, _v2;
static const DWORD FVF; }; const DWORD MultiTexVertex::FVF = D3DFVF_XYZ | D3DFVF_TEX3;
Обратите внимание, что она содержит три набора координат текстур.
Функция Setup выполняет следующие действия:
Заполняет вершинный буфер данными квадрата.
Компилирует пиксельный шейдер.
Создает пиксельный шейдер.
Загружает текстуры.
Устанавливает матрицу проекции и отключает освещение.
Получает дескрипторы объектов выборки.
Получает описания объектов выборки.
bool Setup() { HRESULT hr = 0;
// // Создание квадрата //
Device->CreateVertexBuffer( 6 * sizeof(MultiTexVertex), D3DUSAGE_WRITEONLY, MultiTexVertex::FVF, D3DPOOL_MANAGED, &QuadVB, 0);
MultiTexVertex* v = 0; QuadVB->Lock(0, 0, (void**)&v, 0);
v[0] = MultiTexVertex(-10.0f, -10.0f, 5.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f); v[1] = MultiTexVertex(-10.0f, 10.0f, 5.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); v[2] = MultiTexVertex( 10.0f, 10.0f, 5.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f);
v[3] = MultiTexVertex(-10.0f, -10.0f, 5.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f); v[4] = MultiTexVertex( 10.0f, 10.0f, 5.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f); v[5] = MultiTexVertex( 10.0f, -10.0f, 5.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);
QuadVB->Unlock();
// // Компиляция шейдера //
ID3DXBuffer* shader = 0; ID3DXBuffer* errorBuffer = 0;
hr = D3DXCompileShaderFromFile( "ps_multitex.txt", 0, 0, "Main", // имя точки входа "ps_1_1", D3DXSHADER_DEBUG, &shader, &errorBuffer, &MultiTexCT);
// Выводим любые сообщения об ошибках if(errorBuffer) { ::MessageBox(0, (char*)errorBuffer->GetBufferPointer(), 0, 0); d3d::Release<ID3DXBuffer*>(errorBuffer); }
if(FAILED(hr)) { ::MessageBox(0, "D3DXCompileShaderFromFile() - FAILED", 0, 0); return false; }
// // Создание пиксельного шейдера //
hr = Device->CreatePixelShader( (DWORD*)shader->GetBufferPointer(), &MultiTexPS);
if(FAILED(hr)) { ::MessageBox(0, "CreateVertexShader - FAILED", 0, 0); return false; }
d3d::Release<ID3DXBuffer*>(shader);
// // Загрузка текстур //
D3DXCreateTextureFromFile(Device, "crate.bmp", &BaseTex); D3DXCreateTextureFromFile(Device, "spotlight.bmp", &SpotLightTex); D3DXCreateTextureFromFile(Device, "text.bmp", &StringTex);
// // Установка матрицы проекции //
D3DXMATRIX P; D3DXMatrixPerspectiveFovLH( &P, D3DX_PI * 0.25f, (float)Width / (float)Height, 1.0f, 1000.0f);
Device->SetTransform(D3DTS_PROJECTION, &P);
// // Запрещение освещения //
Device->SetRenderState(D3DRS_LIGHTING, false);
// // Получение дескрипторов //
BaseTexHandle = MultiTexCT->GetConstantByName(0, "BaseTex"); SpotLightTexHandle = MultiTexCT->GetConstantByName(0, "SpotLightTex"); StringTexHandle = MultiTexCT->GetConstantByName(0, "StringTex");
// // Получение описания констант //
UINT count; MultiTexCT->GetConstantDesc( BaseTexHandle, &BaseTexDesc, &count); MultiTexCT->GetConstantDesc( SpotLightTexHandle, &SpotLightTexDesc, &count); MultiTexCT->GetConstantDesc( StringTexHandle, &StringTexDesc, &count);
MultiTexCT->SetDefaults(Device);
return true; }
Функция Display устанавливает пиксельный шейдер, разрешает использование трех текстур и устанавливает для них требуемые режимы выборки перед визуализацией квадрата.
bool Display(float timeDelta) { if(Device) { // // ...код обновления камеры пропущен //
// // Визуализация //
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0); Device->BeginScene();
// Установка пиксельного шейдера Device->SetPixelShader(MultiTexPS); Device->SetFVF(MultiTexVertex::FVF); Device->SetStreamSource(0, QuadVB, 0, sizeof(MultiTexVertex));
// Базовая текстура Device->SetTexture(BaseTexDesc.RegisterIndex, BaseTex); Device->SetSamplerState(BaseTexDesc.RegisterIndex, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(BaseTexDesc.RegisterIndex, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(BaseTexDesc.RegisterIndex, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
// Текстура прожектора Device->SetTexture(SpotLightTexDesc.RegisterIndex, SpotLightTex); Device->SetSamplerState(SpotLightTexDesc.RegisterIndex, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(SpotLightTexDesc.RegisterIndex, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(SpotLightTexDesc.RegisterIndex, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
// Текстура с текстом Device->SetTexture( StringTexDesc.RegisterIndex, StringTex); Device->SetSamplerState(StringTexDesc.RegisterIndex, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(StringTexDesc.RegisterIndex, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(StringTexDesc.RegisterIndex, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
// Рисуем квадрат Device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);
Device->EndScene(); Device->Present(0, 0, 0, 0); } return true; }
И, конечно, следует помнить о необходимости освобождения полученных интерфейсов в функции Cleanup:
void Cleanup() { d3d::Release<IDirect3DVertexBuffer9*>(QuadVB);
d3d::Release<IDirect3DTexture9*>(BaseTex); d3d::Release<IDirect3DTexture9*>(SpotLightTex); d3d::Release<IDirect3DTexture9*>(StringTex);
d3d::Release<IDirect3DPixelShader9*>(MultiTexPS); d3d::Release<ID3DXConstantTable*>(MultiTexCT); }
Координаты для нескольких текстур
18.1.2. Координаты для нескольких текстур
Вспомните, в главе 6 говорилось, что для каждого трехмерного треугольника мы определяем соответствующий треугольный фрагмент текстуры, который накладывается на трехмерный треугольник. Мы делаем это путем добавления к данным каждой вершины координат текстуры. Таким образом, три вершины определяющие треугольник, определяют и соответствующий треугольный фрагмент текстуры.
Поскольку теперь мы используем несколько текстур, для каждой из трех определяющих треугольник вершин нам требуется определение соответствующего треугольного фрагмента каждой из используемых текстур. Мы делаем это добавляя наборы координат текстуры в данные каждой вершины — по одному набору координат для каждой используемой текстуры. Например, если мы хотим смешивать вместе три текстуры, то у каждой вершины должны быть три набора координат текстуры, индексирующих три используемые текстуры. Таким образом, структура данных вершины для мультитекстурирования с использованием трех текстур может выглядеть так:
struct MultiTexVertex { MultiTexVertex(float x, float y, float z, float u0, float v0, float u1, float v1, float u2, float v2) { _x = x; _y = y; _z = z; _u0 = u0; _v0 = v0; _u1 = u1; _v1 = v1; _u2 = u2; _v2 = v2; }
float _x, _y, _z; float _u0, _v0; // Координаты текстуры накладываемой на этапе 0. float _u1, _v1; // Координаты текстуры накладываемой на этапе 1. float _u2, _v2; // Координаты текстуры накладываемой на этапе 2.
static const DWORD FVF; }; const DWORD MultiTexVertex::FVF = D3DFVF_XYZ | D3DFVF_TEX3;
Обратите внимание на использованный в настраиваемом формате вершин флаг D3DFVF_TEX3, который указывает, что структура вершины содержит три набора координат текстур. Фиксированный конвейер поддерживает до восьми наборов координат текстур. Если этого вам мало, придется использовать объявление вершин и программируемый конвейер.
ПРИМЕЧАНИЕ
Написание и компиляция пиксельного шейдера
18.3.1. Написание и компиляция пиксельного шейдера
Мы компилируем пиксельный шейдер точно так же, как компилировали вершинные шейдеры. Сперва мы должны написать программу пиксельного шейдера. В этой книге мы пишем наши шейдеры на HLSL. Как только исходный код шейдера написан, мы можем скомпилировать его с помощью функции D3DXCompileShaderFromFile, как описано в разделе16.2. Вспомните, что эта функция возвращает указатель на интерфейс ID3DXBuffer, который содержит скомпилированный код шейдера.
ПРИМЕЧАНИЕ
Объекты выборки в HLSL
Выборка для текстур в пиксельном шейдере осуществляется с помощью специальных встроенных функций HLSL tex*.
ПРИМЕЧАНИЕ
В общем случае эти функции требуют, чтобы мы указали две вещи:
Координаты текстуры (u, v), используемые для индексации элементов текстуры.
Текстуру в которой выполняется индексация.
Координаты текстуры (u, v), как вы помните, передаются во входных данных пиксельного шейдера. Текстура, элемент которой нам надо получить, идентифицируется в пиксельном шейдере с помощью специального объекта HLSL, называемого объект выборки (sampler). Можно думать об объектах выборки как об объектах, идентифицирующих текстуру и этап выборки. Для примера предположим, что мы используем три этапа текстурирования, а значит в пиксельном шейдере нам требуется возможность ссылаться на каждый из этих этапов. Для этого в пиксельном шейдере напишем:
sampler FirstTex; sampler SecondTex; sampler ThirdTex;
Direct3D свяжет каждый из этих объектов выборки с уникальным этапом выборки. Затем в приложении мы определяем этап, которому соответствует объект выборки и устанавливаем для этого этапа требуемые текстуру и режимы выборки. Приведенный ниже код показывает, как приложение может установить текстуру и режимы выборки для FirstTex:
// Создание текстуры IDirect3DTexture9* Tex; D3DXCreateTextureFromFile(Device, "tex.bmp", &Tex); . . . // Получение дескриптора константы FirstTexHandle = MultiTexCT->GetConstantByName(0, "FirstTex");
//Получение описания константы D3DXCONSTANT_DESC FirstTexDesc; UINT count; MultiTexCT->GetConstantDesc(FirstTexHandle, &FirstTexDesc, &count); . . . // Устанавливаем текстуру/режимы выборки для // объекта выборки FirstTex. Мы определяем этап с которым // связан FirstTex по значению члена данных // D3DXCONSTANT_DESC::RegisterIndex: Device->SetTexture(FirstTexDesc.RegisterIndex, Tex);
Device->SetSamplerState(FirstTexDesc.RegisterIndex, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(FirstTexDesc.RegisterIndex, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(FirstTexDesc.RegisterIndex, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
ПРИМЕЧАНИЕ
Основы мультитекстурирования
Мультитекстурирование, возможно, является самой простой из техник, которые можно реализовать с помощью пиксельного шейдера. Более того, поскольку пиксельные шейдеры заменяют стадию мультитекстурирования фиксированного конвейера, нам необходимо иметь хотя бы базовое представление о том, что происходит на этапе мультитекстурирования. Данный раздел представляет собой краткий обзор мультитекстурирования.
Когда ранее, в главе6, мы обсуждали наложение текстур, обсуждение мультитекстурирования в фиксированном конвейере функций было пропущено по двум причинам. Во-первых, мультитекстурирование это часть более сложного процесса и мы посчитали, что в тот момент эта тема была бы слишком сложна для восприятия. Во-вторых, фиксированные функции этапа мультитекстурирования заменяются новыми и более мощными пиксельными шейдерами; следовательно имеет смысл не тратить время на устаревшие фиксированные функции этапа мультитекстурирования.
Идея, лежащая в основе мультитекстурирования отчасти связана со смешиванием. В главе 7 мы узнали о смешивании растеризуемых пикселей с пикселями ранее записанными во вторичный буфер для реализации некоторых эффектов. Теперь мы расширим эту идею для нескольких текстур. Итак, мы разрешаем одновременное использование нескольких текстур и затем определяем, как эти текстуры должны быть смешаны одна с другой для получения требуемого эффекта. Наиболее часто мультитекстурирование используется для освещения. Вместо того, чтобы воспользоваться моделью освещения Direct3D на этапе обработки вершин, мы применяем специальные карты текстур, называемые картами освещения (light map), которые сообщают о том, как освещена поверхность. Представим, например, что нам надо осветить прожектором большой ящик. Мы можем описать прожектор в виде структуры D3DLIGHT9, или можно смешать вместе карту текстур, представляющую ящик и карту освещения, представляющую прожектор, как показано на рис 18.1.