Урок 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. Архитектура вершинного шейдера

Как вы сами хорошо понимаете, вершинный шейдер состоит не только из набора команд, но и из переменных - регистров, которыми эти команды оперируют. Всего мы имеем пять различных видов регистов:

  1. Входные регистры (Input Vector) v0, v1, v2...
  2. Константные регистры c0,c1,c2...
  3. Временные регистры r0,r1,r2...
  4. Адресный регистр a0
  5. Выходные регистры

Например, команда 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-файл можно взять здесь.

Hosted by uCoz