Урок
3: Использование вершинных шейдеров
Наконец-то мои длинные, "программисткие" руки добрались до вершинных шейдеров. Все таки уже давно хотелось попробовать, пощупать, оценить, так сказать, их потенциальные возможности. И тем более, после официального анонса GeForce 3, который аппаратно полностью поддерживает DirectX 8.0, пора рассказать всему миру, что это за зверь такой - вершинный шейдер. Итак, начнем.
Как известно, шейдер - это небольшая микропрограмма, которая обрабатывает поток входных графических данных. Под потоком следует понимаеть конечное количество треугольников, состоящих из вершин, которые нужно нарисовать. Т.е. вершинный шейдер обрабатывает только вершины, а точнее информацию о них. Шейдер пишется на языке, подобном ассемблеру, и исполняется либо графическим чипом видеокарты, либо центральным процессором в режиме программной эмуляции.
Так в чем же дело, спросите вы, почему такая шумиха вокруг DirectX 8.0 и нового GeForce 3, который называют революцией в области компьютерной графики? Все очень просто, достаточно взглянуть на рисунок.
Рисунок 1. Архитектура графического конвейера Direct3D
Раньше разработчики игр были сильно ограничены фиксированными возможностями блока трансформации и освещения вершин (Transformation and Lighting Engine). В их распоряжении было всего три матрицы преобразования (см. Урок 2) и максимум 8 источников света. На смену этому блоку в новой архитектуре Direct3D пришел Вершинный шейдер (Vertex Shader), который полностью является программируемым. Получается, что теперь перед разработчиком сняты все ограничения и он может реализовать при помощи шейдеров любые матричные преобразования и любую модель освещения. Для большей убедительности скажу, что только благодаря шейдерам Id Software удалось добится такой удивительной графики в Doom III.
Ну да ладно, хватит мечтать. Такого уровня графики нам пока с вами не достичь, поэтому, возмем за основу предыдущий пример и попытаемся хотя бы получить такие же вращающие фигуры.
Рисунок 2. Архитектура вершинного шейдера
Как вы сами хорошо понимаете, вершинный шейдер состоит не только из набора команд, но и из переменных - регистров, которыми эти команды оперируют. Всего мы имеем пять различных видов регистов:
Например, команда mov r0, v0 переносит данные из входного регистра v0 во временный регистр r0. Также хотелось бы еще отметить, что каждый регистр представляет собой вектор, т.е. состоит из четырех float-чисел. Следовательно, матрицу 4x4 можно разместить в четырех регистрах.
Но вернемся к нашей задаче. Чтобы нам получить такие же вращающие фигуры, как в предыдущем примере, нужно просто последовательно умножить позицию каждой вершины на три матрицы преобразования: мировую, видовую и проекционную. Поэтому, я написал небольшой и простой шейдер.
vs.1.0
;-------------------------------------------------------------------------
; Константные регистры, содержащие матрицы преобразования
; c0-c3 = мировая матрица
; c4-c7 = видовая матрица
; c8-c11 = проекционная матрица
;
; Компоненты вершины (определяются в Декларации Вершинного шейдера)
; v0 = Позиция
; v5 = Цвет
;-------------------------------------------------------------------------
;-------------------------------------------------------------------------
; Преобразование вершины
;-------------------------------------------------------------------------
; Мировое преобразование
m4x4 r0, v0, c0
; Видовое преобразование
m4x4 r0, r0, c4
; Проекционное преобразование
m4x4 r0, r0, c8
; Сохраняем преобразованную позицию вершины в выходной регистр позиции
mov oPos, r0
; Сохраняем цвет вершины в выходной регистр цвета
mov oD0, v5
Как видно, с помощью трех команд m4x4, выполняются наши матричные преобразования. В выходной регистр oPos записывается полученная позиция вершины, а в регистр oD0 - ее цвет. Но чтобы производить эти действия, надо все три матрицы предварительно занести в константные регистры c0-c11, для чего мы в программе немного изменим функцию установки матриц.
//
Функция SetMatrices() устанавливает матрицы преобразований
void SetMatrices(void)
{
D3DXMATRIX matWorld;
D3DXMATRIX matView;
D3DXMATRIX matProj;
D3DXMATRIX mat;
// Расчет мировой матрицы
D3DXMatrixRotationY(&matWorld,timeGetTime()/500.0f);
// Расчет видовой матрицы
D3DXMatrixLookAtLH(&matView,&D3DXVECTOR3(0.0f,0.0f,-3.5f),
&D3DXVECTOR3(0.0f,0.0f,0.0f),
&D3DXVECTOR3(0.0f,1.0f,0.0f));
// Расчет проектной матрицы
D3DXMatrixPerspectiveFovLH(&matProj,D3DX_PI/4,1.0f,1.0f,100.0f);
// Заносим матрицы в вершинный шейдер
D3DXMatrixTranspose(&mat,&matWorld);
d3d_device->SetVertexShaderConstant(0,&mat,4);
D3DXMatrixTranspose(&mat,&matView);
d3d_device->SetVertexShaderConstant(4,&mat,4);
D3DXMatrixTranspose(&mat,&matProj);
d3d_device->SetVertexShaderConstant(8,&mat,4);
}
И для того, чтобы использовать шейдер в нашей программе, необходимо его сперва проинициализировать.
//
Функция InitVertexShaders() инициализирует вершинный шейдер
void InitVertexShaders(void)
{
LPD3DXBUFFER pShaderCode;
DWORD dwShaderDecl[] = {
D3DVSD_STREAM(0),
D3DVSD_REG(D3DVSDE_POSITION,D3DVSDT_FLOAT3),
D3DVSD_REG(D3DVSDE_DIFFUSE,D3DVSDT_D3DCOLOR),
D3DVSD_END()
};
HRESULT hr;
// Компиляция файла вершинного шейдера
hr = D3DXAssembleShaderFromFile("standart.vsh",
0,NULL,&pShaderCode,NULL);
// Создаем вершинный шейдер
hr = d3d_device->CreateVertexShader(dwShaderDecl,
(DWORD*)pShaderCode->GetBufferPointer(),&vertex_shader,0);
//
pShaderCode->Release();
};
В
первом шаге, происходит компиляция вершинного шейдера из файла, в котором
находится его исходный текст, в буфер, который будет содержать уже машинный
код шейдера. Вторым шагом, мы уже создаем вершинный шейдер, с указанием на
раздел его декларации dwShaderDecl и полученного
перед этим буфера pShaderCode.
Немного поясню, в разделе декларации вершинного шейдера устанавливается связь
между данными, поступающими из потока, и соответствующими входными регистрами
шейдера. В данном случае мы указываем, что позиция вершины из буфера вершин
будет записываться в регистр v0. а цвет - в регистр
v5.
Теперь, в функции прорисовки сцены RenderDirect3D()
мы должны установить для устройства Direct3D наш вершинный шейдер.
d3d_device->SetVertexShader(vertex_shader);
Вот и все, мы получим наши знакомые два вращающихся объекта. Для эксперимента также предлагаю вам заменит последнюю строчку в файле standart.vsh на mov oD0, c0. Получите интресный визуальный эффект. Полный исходный текст программы и exe-файл можно взять здесь.