Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Как самому создать компьютерную игру #16


Информационный Канал Subscribe.Ru

Как самому создать компьютерную игру
Выпуск 16

Сайт рассылки - www.GameMaker.ru 
Адрес для обратной связи - gamemaker@pisem.net

Приветствую всех, кто читает эту рассылку!

Сегодня в выпуске:
1. Вместо предисловия.
2. Вертексные шейдеры.
3. Послесловие.



Вместо предисловия.

      Перед вами очередной выпуск рассылки. Так получилось, что материал этого выпуска уже публиковался на одном из сайтов сети, хотя и был написан специально для рассылки. Поэтому будем считать, что это просто пердисловие к следующему выпуску. Своеобразный разогрев. Ну а раз так, то не буду утомлять вас долгими речами. Перейдем к делу...
      Сегодня статью для вас подготовил наш специалист по компьютерной графике. :) Вы его скорее всего помните по выпуску 14 (статью про партиклы). Встречайте... :)


Вертексные шейдеры.

Немного.

⌠110001010011■

3155

Ну вот. Очередной винт отформатирован, пройден очередной левел в любимом гамесе и теперь можно перейти к делам. Поехали.

О шейдерах и кроликах.

⌠Кролик √ это не только ценный мех┘■

известное

О кроликах вроде все. Давайте слегка углубимся в историю. Были далёкие 70е. Все дрыгались под ритмы диско и живали джусифрут. Можно считать, что эти годы явились родоначальниками компьютерной графики. Большинство алгоритмов современной нынешней графики были придуманы именно в конце 70х √ 80х годах. Это BumpMapping, свет, закраска по Фонгу/Гуро, Z-буффер, разные там сглаживания и даже никому не известные (по названию) алгоритмы Брезенхейма (рисования линий, кругов и т.д.), но которыми все пользуются, были придуманы до любимого паскаля. Кому интересно есть книга Роджерса ⌠Алгоритмические основы компьютерной графики■ 1989г.

Наступили менее далёкие 90е. 3Д графика начала переходить из разряда на-супер-калькуляторе-у-неизвестного-матиматика в категорию а-у-меня-это-есть-на-компе. С трудом перевалив через середину, 90е породили на свет общедоступные так называемые ⌠3Д акселераторы■, и про то, что раньше всё рисовали исключительно ЦПУ, все забыли - теперь треугольнички рисовали ГПУ типа Voodoo (Glide-рулёз), а после в игру вступила nVidia и ATi. Когда же 90е вышли на финальную прямую, народ сообразил, что считать матрицы ЦПУ не должен. Даже знаменитый ММХ не стоит грузить на столько грязной работой, и тут же родилась фигня, которую назвали ⌠Блок T&L■ (кто не знает √ это transforming & lighting). И на пару лет все успокоились.

И всё было бы хорошо, если бы не один грешок этого самого T&L √ он выполнял последовательно один и тот же набор операций, как нужных, так и не нужных (это умножение на 3 матрицы трансформации, расчёт света и всё в таком вот духе). А если мне надо сначала расчитать свет ╧1, затем помножить на матрицу ╧1, потом рассчитать свет ╧2 и помножить на матрицы ╧3 и ╧2? ⌠Делать нечего, бояре┘■, придётся подключать в работу ЦПУ. Плохо. Осознав это, собрался народ со всех уголков трёхмерного мира, nVidia сказала всё, что думает об ATi, ATi тоже в накладе не осталась, а там и вся остальная Силиконавая долина поделилась своим мнением. Чуть успокоившись, они подумали и додумались до не безызвестных шейдеров. Были придуманы и вертексные и пиксельные шейдеры.

На рисунке чётко видно, на какой стадии используется vertex shader, а где pixel shader:

Схема Pipeline (взято с www.gamedev.net)

Я не буду сейчас рассказывать о пиксельных шейдерах (это не только из-за вредности, а из-за того, что с ними не до конца разобрался), о них я скажу как-нить потом. Сейчас будут только вертексные шейдеры (ВШ).

Итак. ВШ √ это небольшая ассемблерная программа для обработки ОДНОЙ вершины. Лично я понял ЧТО конкретно они делают когда ознакомился с перечнем доступных средств (синтаксисом).

Синтаксис.

⌠Мы писали, мы писали, наши пальчики устали┘■

любимая школьная песенка

Синтаксис команд:

op dest, src0,src1,src2 где

op √ команда,

dest - регистр, куда записываются результаты команды,

scr[0,1,2] - входные регистры, количество которых зависит от конкретной команды.

Основные команды:

Команда

Описание

Количество тактов

add

   сумма

1

dp3

   трех-компонентный dot-product

1

dp4

   четырех-компонентный dot-product

1

dst

   вектор расстояния

1

expp

   экспоненциал 10-битной точности

1

lit

   световой коэффициент

1

logp

   логарифм 10-битной точности

1

mad

   произведение и сложение

1

max

   максимум

1

min

   минимум

1

mov

   копирование

1

mul

   произведение

1

rcp

   обратная величина

>1

rsq

   обратный квадратный корень

>1

sge

   установить, если больше или равно

1

slt

   установить, если меньше

1

sub

   Разность

1

Макрокоманды:

Команда

Описание

Количество тактов

exp

   экспоненциал двойной точности

12

frc

   доля

3

log

   логарифм двойной точности

12

m3x2

   3x2 произведение вектора и матрицы

2

m3x3

   3x3 произведение вектора и матрицы

3

m3x4

   3x4 произведение вектора и матрицы

4

m4x3

   4x3 произведение вектора и матрицы

3

m4x4

   4x4 произведение вектора и матрицы

4

Другие команды

Команда

Описание

Количество тактов

def

   определить константу

  -▒▓-

vs

   версия и тип шейдера

  -▒▓-

Что касается переменных (ресурсов):

Тип ресурса

Макс. количество

   Входные регистры (Input registers)

16

   Константные регистры (Constant registers)

96

   Временные регистры (Temp registers)

12

   Адресный регистр (Address register)

1

   Выходные регистры (Output registers)

зависит от реализации

   Максимальное количество команд

128

Картинка (для наших маленьких читателей J ):

Допустим вы рендируете модель с помощью DrawIndexPrimitive. Модель состоит из n-ого числа вертексов. Если вы рендируете с помощью обычных средств, то сначала надо задать по отдельности матрицу вида, проекции и мировую, затем задать свет и теперь рендировать. Обработка каждой вершины пойдёт по заранее запланированной программе. Теперь давайте посмотрим, как пойдёт дело при использовании ВШ.

Простейший ВШ:

vs.1.1 ; версия ВШ
dp4
oPos.x, v0, c0 ; Обычное произведение вектора на матрицу
dp4
oPos.y, v0, c1 ; oPos - выходной регистр позиции, т.е. позиция вертекса в 3Д

dp4 oPos.z, v0, c2
dp4 oPos
.w, v0, c3
mov oD0, c4 ; oD0-выходной регистр цвета. Сюда заносится цвет вертекса

Теперь что где что значит:

v0 √ первоначальный вертекс (вектор {x,y,z,w}). Входной регистр.

c[0,1,2,3] √ константные регистры, в которых содержится матрица преобразования.

c4 - константный регистр. Цвет.

Уточнение: константные регистры задаются пользователем.

Т.о. Каждый вертекс проходит заданную именно нами обработку, прежде чем попасть на растеризацию. Такой подход обеспечивает большую гибкость и масштабируемость средств визуализации.

Подробнее о работе.

⌠Только упорный труд может сделать из обезьяны слесаря 6-ого разряда■

из Фоменковских афоризмов

Давайте, штоль, попробуем что-то сделать? Я, пожалую, не буду изощряться, и придумывать что-то новое, а возьму пример из www.Gamedev.net . В этом примере будем крутить выпуклый квадрат, и там ещё будет анизотропное освещение.

Чтоб упростить задачу предлагаю использовать Микрософтный прибамбас для таких вещей, именуемый как класс CD3Dapplication. Для тех, кто не знаком с этим достижением мысли инженеров известной конторы, поясню. Это, так сказать, базовый набор функций для любого приложения (нечто типа MFC для создания приложений типа компьютерных игр). Этот класс создаёт главное окно, реализует обработку виндоусовских мессаг, и вызывает необходимый набор функций, в которые входят:

  • OneTimeSceneInit() √ инициализация приложения(вызывается единожды в самом начале).
  • InitDeviceObjects() √ инициализация 3Д объектов(загрузка текстур и т.д.).
  • FrameMove() √ анимация, вызывается перед рендерингом, в качестве 2х входных параметров используется время, прошедшее с начала запуска программы и время, прошедшее с последнего запуска этой процедуры(всё в сек., тип √ float).
  • Render() √ без комментариев.
  • DeleteDeviceObjects() - To cleanup the 3D scene objects.
  • FinalCleanup() √ финальный cleanup, вызывается при выходе из программы.
  • MsgProc() √ виндоусные мессаги, думаю объяснять не надо.

Вот так. Т.о. чтоб всё это использовать, надо создать свой класс, наследуемый от CD3Dapplication и в нём уже реализовать перечисленные выше функции:

class MyApp : public CD3Dapplication

{

}

Чтоб включить эту машину смерти, достаточно в main создать наш класс и вызвать функцию Run этого класса:

int APIENTRY WinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR lpCmdLine,

int nCmdShow)

{

MyApp App;

return App.Run();

}

Теперь всю грязную работу выполнит CD3Dapplication, ну а нам остаётся только реализовать пару функций.

Для начала создадим Win32 проект. Опишем класс CMyD3DApplication:

class CMyD3DApplication : public CD3DApplication

{

DWORD m_dwVertexShader;

DWORD m_dwSizeofVertices, m_dwSizeofIndices;

LPDIRECT3DVERTEXBUFFER8 m_pVB;

LPDIRECT3DINDEXBUFFER8 m_pIB;

D3DXMATRIX m_matWorld, m_matView, m_matProj;

POINT m_oldCursorPos;

LPDIRECT3DTEXTURE8 m_pTexture;

CD3DArcBall m_ArcBall;

FLOAT m_fObjectRadius;

FLOAT m_fZDist;

D3DXVECTOR4 m_vLightColor[3];

BOOL m_bKey[300];

HRESULT ConfirmDevice( D3DCAPS8*, DWORD, D3DFORMAT );

HRESULT CMyD3DApplication::CreateVSFromCompiledFile (IDirect3DDevice8* m_pd3dDevice, DWORD* dwDeclaration, TCHAR* strVSPath, DWORD* m_dwVS);

protected:

HRESULT OneTimeSceneInit();

HRESULT InitDeviceObjects();

HRESULT RestoreDeviceObjects();

HRESULT InvalidateDeviceObjects();

HRESULT DeleteDeviceObjects();

HRESULT FinalCleanup();

HRESULT Render();

HRESULT FrameMove();

HRESULT MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

public:

CMyD3DApplication();

};

Здесь надо сразу оговорится про одну интересную фичу из набора DX SDK √ класс CD3DArcBall. Он делает работу по расчету матрицы поворота и перемещения, которые осуществляются с помощью мыши (в симплах от SDK, где что-то можно вращать мышью, используется именно этот класс). Вращение выглядит как перемещение ⌠точки глаза■ по шару, описанному вокруг определённой точки. Чтоб его использовать, достаточно задать параметр √ радиус, и передавать классу мессаги, относительно мыши.

В качестве описания каждой точки будем использовать структуру:

struct VERTEX

{

FLOAT x, y, z; // The untransformed position for the vertex

FLOAT nx, ny, nz; // the normal

FLOAT u, v;

};

и

#define D3DFVF_VERTEX (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_TEX1)

 

Поехали описывать функции:

CMyD3DApplication::CMyD3DApplication()

{

m_dwVertexShader = 0xffffffff;

m_dwCreationWidth = 700;

m_dwCreationHeight = 500;

m_fObjectRadius = 1;

m_fZDist = 4;

}

Вроде всё интуитивно понятно. OneTimeSceneInit нам не понадобится пока, как, впрочем и InvalidateDeviceObjects:

HRESULT CMyD3DApplication::OneTimeSceneInit()

{

return S_OK;

}

HRESULT CMyD3DApplication::InvalidateDeviceObjects()

{

return S_OK;

}

Чтоб использовать шейдеры, их надо загрузить. Тут есть есть два пути:

    1. Использовать набор команд в текстовом виде (либо прописать в программе, либо загружать из файла).
    2. Загружать уже откомпилированный код.

Я пошёл вторым путём (так сложнее и правильнее?). Если кто хочет поступить в соответствии с первым вариантом, могут сделать так:

LPD3DXBUFFER pVS;

res = D3DXAssembleShader( BasicVertexShader , sizeof(BasicVertexShader) -1,

0 , NULL , &pVS , &pErrors );

res = m_pd3dDevice->CreateVertexShader( dwDec1, (DWORD*)pVS->GetBufferPointer(),

&m_dwVertexShader, 0 );

Где BasicVertexShader √ массив, в котором храниться шейдерная программа, про dwDec1 чуть дальше.

Путь номер два. Во-первых надо откомпилить шейдерную программу (ШП). Я использовал для этого nvasm.exe. Чем хорош VisualC++ это тем, что избавляет программиста от многих лишних нажатий на кнопки. Так что если вы используете именно его, то достаточно включить файл с ШП в ваш проект, зайти в его (файла) свойства, найти поле ⌠Command line■ и в нём написать ⌠nvasm myfile.vsh■, где myfile.vsh √ ваш файл с ШП. А поле ⌠Outputs■ заполнить страшной строкой ⌠$(InputName).pso■. Думаю если кто об этом не знал, всё понял.

Откомпилили. Теперь дальше. Грузим:


HRESULT CMyD3DApplication::InitDeviceObjects()

{

// loads a *.vso binary file, already compiled with NVASM and

// creates a vertex shader

if (FAILED(CreateVSFromCompiledFile (m_pd3dDevice, dwDecl, "test1.vso", &m_dwVertexShader)))

return E_FAIL;

return S_OK;

}

HRESULT CMyD3DApplication::CreateVSFromCompiledFile (IDirect3DDevice8* m_pd3dDevice,

DWORD* dwDeclaration,

TCHAR* strVSPath,

DWORD* m_dwVS)

{

char szBuffer[128]; // debug output

DWORD* dwpVS; // pointer to address space of the calling process

HANDLE hFile, hMap; // handle file and handle mapped file

TCHAR tempVSPath[512]; // temporary file path

HRESULT hr; // error

if( FAILED( hr = DXUtil_FindMediaFile( tempVSPath, strVSPath ) ) )

return D3DAPPERR_MEDIANOTFOUND;

hFile = CreateFile( tempVSPath, GENERIC_READ,0,0,OPEN_EXISTING,

FILE_ATTRIBUTE_NORMAL,0);

if(hFile != INVALID_HANDLE_VALUE)

{

if(GetFileSize(hFile,0) > 0)

hMap = CreateFileMapping(hFile,0,PAGE_READONLY,0,0,0);

else

{

CloseHandle(hFile);

return E_FAIL;

}

}

else

return E_FAIL;

// maps a view of a file into the address space of the calling process

dwpVS = (DWORD *)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);

// Create the vertex shader

hr = m_pd3dDevice->CreateVertexShader( dwDeclaration, dwpVS, m_dwVS, 0 );

if ( FAILED(hr) )

{

OutputDebugString( "Failed to create Vertex Shader, errors:\n" );

D3DXGetErrorStringA(hr,szBuffer,sizeof(szBuffer));

OutputDebugString( szBuffer );

OutputDebugString( "\n" );

return hr;

}

UnmapViewOfFile(dwpVS);

CloseHandle(hMap);

CloseHandle(hFile);

return S_OK;

}

 

Теперь скажу про dwDec1. Как известно, в ШП есть входные регистры, и именно какой регистр что описывает и показывает dwDec1:

DWORD dwDecl[] =

{

D3DVSD_STREAM(0),

D3DVSD_REG(0, D3DVSDT_FLOAT3 ), // position in space

D3DVSD_REG(3, D3DVSDT_FLOAT3 ), // normals

D3DVSD_REG(7, D3DVSDT_FLOAT2), // tex coord

D3DVSD_END()

};

Т.о. во входном регистре v0 будут координаты точки, в регистре v3 √ нормали, а в v7 просто координаты текстуры (в соответствии со структурой VERTEX). Функцию CreateVSFromCompiledFile описывать не буду √ кто захочет, разберётся.

Всё остальное по загрузке я сунул в RestoreDeviceObjects. Предлагаю разобраться, что там к чему.

Во-первых, создаём фигуру (что будем рисовать). Это будет сетка размерности NxM секторов на куске плоскости XY (-1, -1; 1, 1) (т.е. квадрат). В качестве высоты каждого узла сетки будем использовать функцию:

F(x,y) = sin(x)* sin (y), где x,yО[0;p ]

Создаём массив точек:

#define WIDTH 30

#define HEIGHT 30

#define TO_RAD(x) (x*3.14/180)

float GetHeight(float x, float y)

{

return sin(TO_RAD(x*180./WIDTH))*sin(TO_RAD(y*180./HEIGHT));

}

VERTEX Vertices[(WIDTH+1)*(HEIGHT+1)];

float kx = 2./WIDTH;

float ky = 2./HEIGHT;

float ktx = 1./WIDTH;

float kty = 1./HEIGHT;

for(int x=0;x<WIDTH+1;x++)

{

for(int y=0;y<HEIGHT+1;y++)

{

// position in space

Vertices[x*(WIDTH+1)+y].x = x*kx-1;

Vertices[x*(WIDTH+1)+y].y = y*ky-1;

Vertices[x*(WIDTH+1)+y].z = GetHeight(x,y)/2;

// normal

Vertices[x*(WIDTH+1)+y].nx = x*kx-1;

Vertices[x*(WIDTH+1)+y].ny = y*ky-1;

Vertices[x*(WIDTH+1)+y].nz = GetHeight(x,y)/2;

// tex coord

Vertices[x*(WIDTH+1)+y].u = 1-x*ktx;

Vertices[x*(WIDTH+1)+y].v = 1-y*kty;

}

}

m_dwSizeofVertices = sizeof (Vertices);

Формируем треугольники:

WORD wIndices[WIDTH*HEIGHT*6], *ind;//={0, 1, 2, 0, 2, 3};

ind = wIndices;

for(x=0;x<WIDTH;x++)

{

for(int y=0;y<HEIGHT;y++)

{

*ind = y+1+x*(HEIGHT+1);

ind++;

*ind = y+1+(x+1)*(HEIGHT+1);

ind++;

*ind = y+x*(HEIGHT+1);

ind++;

*ind = y+x*(HEIGHT+1);

ind++;

*ind = y+1+(x+1)*(HEIGHT+1);

ind++;

*ind = y+(x+1)*(HEIGHT+1);

ind++;

}

}

m_dwSizeofIndices = sizeof (wIndices);

Для расчёта нормалей я использовал функцию, которую нагло взял с того же GameDev.net:

ComputeNormals(&Vertices[0], &wIndices[0],

m_dwSizeofIndices/sizeof(WORD)/3,

m_dwSizeofVertices/sizeof(VERTEX));

VOID ComputeNormals (VERTEX *pvVertices, WORD* pdwIndices,

DWORD dwNumOfTriangles, DWORD dwNumOfVertices)

{

// compute face normals

// for every triangle, take three vertices and cross the edges

D3DXVECTOR3* pvFaceNormals;

pvFaceNormals = new D3DXVECTOR3[dwNumOfTriangles];

for(DWORD i = 0; i < dwNumOfTriangles; ++i)

{

int e0=pdwIndices[i*3+0];

int e1=pdwIndices[i*3+1];

int e2=pdwIndices[i*3+2];

D3DXVECTOR3 v0(pvVertices[e0].x, pvVertices[e0].y, pvVertices[e0].z);

D3DXVECTOR3 v1(pvVertices[e1].x, pvVertices[e1].y, pvVertices[e1].z);

D3DXVECTOR3 v2(pvVertices[e2].x, pvVertices[e2].y, pvVertices[e2].z);

D3DXVECTOR3 edge1 = v1-v0;

D3DXVECTOR3 edge2 = v2-v0;

D3DXVec3Cross(&pvFaceNormals[i],&edge1,&edge2);

D3DXVec3Normalize(&pvFaceNormals[i],&pvFaceNormals[i]);

}

for(DWORD c = 0; c < dwNumOfVertices; ++c)

{

D3DXVECTOR3 v(pvVertices[c].x, pvVertices[c].y, pvVertices[c].z);

D3DXVECTOR3 sum(0,0,0);

int shared=0;

for(DWORD i = 0; i < dwNumOfTriangles; ++i)

{

int e0=pdwIndices[i*3+0];

int e1=pdwIndices[i*3+1];

int e2=pdwIndices[i*3+2];

/*D3DXVECTOR3 v0 = pvVertices[e0].vPosition;

D3DXVECTOR3 v1 = pvVertices[e1].vPosition;

D3DXVECTOR3 v2 = pvVertices[e2].vPosition;*/

D3DXVECTOR3 v0(pvVertices[e0].x, pvVertices[e0].y, pvVertices[e0].z);

D3DXVECTOR3 v1(pvVertices[e1].x, pvVertices[e1].y, pvVertices[e1].z);

D3DXVECTOR3 v2(pvVertices[e2].x, pvVertices[e2].y, pvVertices[e2].z);

if(v0==v||v1==v||v2==v)

{

sum+=pvFaceNormals[i];

++shared;

}

}

D3DXVECTOR3 nrml = sum/(float)shared;

D3DXVec3Normalize(&nrml, &nrml);

pvVertices[c].nx = nrml.x;

pvVertices[c].ny = nrml.y;

pvVertices[c].nz = nrml.z;

}

SAFE_DELETE (pvFaceNormals);

}

Дальше всю эту фигню копируем в буфера (сексуально озабоченных прошу не смеяться сильно с резким выделением слюни на монитор) DX:

// Create the vertex buffers with four vertices

if( FAILED( m_pd3dDevice->CreateVertexBuffer( m_dwSizeofVertices,

D3DUSAGE_WRITEONLY , sizeof(VERTEX), D3DPOOL_MANAGED, &m_pVB ) ) )

return E_FAIL;

// lock and unlock the vertex buffer to fill it with memcpy

VOID* pVertices;

if( FAILED( m_pVB->Lock( 0, m_dwSizeofVertices, (BYTE**)&pVertices, 0 ) ) )

return E_FAIL;

memcpy( pVertices, Vertices, m_dwSizeofVertices);

m_pVB->Unlock();

// create index buffer

if(FAILED (m_pd3dDevice->CreateIndexBuffer(m_dwSizeofIndices, 0,

D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIB)))

return E_FAIL;

// fill index buffer

VOID *pIndices;

if (FAILED(m_pIB->Lock(0, m_dwSizeofIndices, (BYTE **)&pIndices, 0)))

return E_FAIL;

memcpy(pIndices, wIndices, m_dwSizeofIndices);

m_pIB->Unlock();

Настройка фокуса:

FLOAT fAspect = m_d3dsdBackBuffer.Width / (FLOAT)m_d3dsdBackBuffer.Height;

D3DXMatrixPerspectiveFovLH( &m_matProj, D3DX_PI/4, fAspect, 1.0f, 100.0f );

D3DXMatrixLookAtLH( &m_matView,

&D3DXVECTOR3( 0.0f, 1.0f,-4.0f ), //from

&D3DXVECTOR3( 0.0f, 0.0f, 0.0f ), //at

&D3DXVECTOR3( 0.0f, 1.0f, 0.0f ));//up

Некоторые другие установки:

// texture settings

m_pd3dDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);

m_pd3dDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);

m_pd3dDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);

m_pd3dDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_DISABLE);

m_pd3dDevice->SetTextureStageState( 1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);

m_pd3dDevice->SetTextureStageState( 0, D3DTSS_MINFILTER, D3DTEXF_LINEAR);

m_pd3dDevice->SetTextureStageState( 0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR);

m_pd3dDevice->SetTextureStageState( 0, D3DTSS_MIPFILTER, D3DTEXF_LINEAR);

m_pd3dDevice->SetTextureStageState( 0, D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP);

m_pd3dDevice->SetTextureStageState( 0, D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP);

// Turn off culling, so we see the front and back of the quad

m_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );

// Turn off D3D lighting, since we are providing our own vertex shader lighting

m_pd3dDevice->SetRenderState( D3DRS_LIGHTING, FALSE );

Текстура:

D3DXCreateTextureFromFile(m_pd3dDevice, "tex1.bmp", &m_pTexture);

CD3DArcBall:

m_ArcBall.SetWindow( m_d3dsdBackBuffer.Width, m_d3dsdBackBuffer.Height, 0.85f );

m_ArcBall.SetRadius( m_fObjectRadius );

Теперь снова о шейдерах. Как я уже говорил, помимо задания входных регистров, можно задать ещё и константы. Реализовываем:

m_pd3dDevice->SetVertexShaderConstant(CLIP_MATRIX, &(m_matWorld*m_matView * m_matProj), 4);

D3DXMATRIX matWorldInverse;

D3DXMatrixInverse(&matWorldInverse, NULL, &m_matWorld);

m_pd3dDevice->SetVertexShaderConstant(INVERSE_WORLD_MATRIX, &matWorldInverse,3);

// light position

FLOAT fLightPosition[3] = {0.0f, 0.0f, 1.0f};

m_pd3dDevice->SetVertexShaderConstant(LIGHT_POSITION, fLightPosition,1);

FLOAT fLC[3] = {0.8,0.4,0.4};

m_pd3dDevice->SetVertexShaderConstant(LIGHT_COLOR, fLC, 1);

m_pd3dDevice->SetVertexShaderConstant(SPEC_POWER, D3DXVECTOR4(0,10,0,0),1);


FLOAT fMaterial[3] = {0.7,0.7,0.7};

m_pd3dDevice->SetVertexShaderConstant(DIFFUSE_COLOR, fMaterial, 1);

Функция SetVertexShaderConstant и служит как раз для задания константных регистров. Первый её параметр √ номер константного регистра (в данном случае задаётся макросом), второй параметр √ указатель, где хранятся значения, которые надо записать в регистры, а последний параметр √ сколько вешать (говорите только точно). Т.о. если нам надо записать вектор (1,2,3,1) в константный регистр ╧4 (zero-base) надо писать:

SetVertexShaderConstant(4, D3DXVECTOR4(1,2,3,1), 1)

А если надо занести матрицу mat1 в константные регистры 2, 3, 4 и 5 надо сделать так:

SetVertexShaderConstant(2, &mat1, 4)

Вообще входные и константные регистры состоят из 4х значений типа FLOAT.

Как видно, в константные регистры передаются общая матрица, инверсная матрица вида (зачем это нужно объясню потом), далее идёт положение источника света, его цвет и отражательная способность объекта, и, наконец, диффузный цвет.

Обычно перед рендерингом обычно осуществляется смена положения объектов в пространстве. Не будем выпендриваться:

HRESULT CMyD3DApplication::FrameMove()

{

static i=0;

D3DXMatrixIdentity(&m_matWorld);

D3DXMatrixMultiply(&m_matWorld, &m_matWorld, m_ArcBall.GetTranslationDeltaMatrix());

D3DXMatrixMultiply(&m_matWorld, &m_matWorld, m_ArcBall.GetRotationMatrix());

D3DXVECTOR3 vEyePt (0.0f, 0.0f, m_fObjectRadius+m_fZDist );

D3DXVECTOR3 vLookAtPt (0.0f, 0.0f, 0.0f);

D3DXVECTOR3 vUp (0.0f, 1.0f, 0.0f);

D3DXMatrixLookAtLH(&m_matView, &vEyePt, &vLookAtPt, &vUp);

// set the clip matrix

m_pd3dDevice->SetVertexShaderConstant(CLIP_MATRIX, &(m_matWorld*m_matView * m_matProj), 4);

D3DXMATRIX matWorldInverse;

D3DXMatrixInverse(&matWorldInverse, NULL, &m_matWorld);

m_pd3dDevice->SetVertexShaderConstant(INVERSE_WORLD_MATRIX, &matWorldInverse,3);

m_pd3dDevice->SetVertexShaderConstant(EYE_VECTOR, -vEyePt,1);

return S_OK;

}

Опять-таки ничего сверхъестественного. Считаем с помощью CD3DXArcBall нужную нам матрицу поворотов и перемещения нашего объекта, задаём инверсированную матрицу вида, а затем делаем вид, что смотрим из точки (0, 0, Z) в точку (0, 0, 0), и, что характерно, центруем ноги перпендикулярно плоскости OXZ (не забыв, что вектор направления нашей головы должен быть (0, 1, 0)).

Тот кто думает, что функция рисования будет огромной будут расстроены / обрадованы (нужное подчеркнуть):

HRESULT CMyD3DApplication::Render()

{

// Clear the viewport

m_pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET, 0x0000008f, 1.0f, 0L );

// Begin the scene

if( SUCCEEDED( m_pd3dDevice->BeginScene() ) )

{

m_pd3dDevice->SetVertexShader( m_dwVertexShader );

m_pd3dDevice->SetTexture(0, m_pTexture);

m_pd3dDevice->SetStreamSource(0, m_pVB, sizeof(VERTEX));

m_pd3dDevice->SetIndices(m_pIB, 0);

m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, (HEIGHT+1)*(WIDTH+1), 0,HEIGHT*WIDTH*2);

// End the scene.

m_pd3dDevice->EndScene();

}

return S_OK;

}

Чтоб это понять, достаточно только знать, как правильно пишется Direkt3D.

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

HRESULT CMyD3DApplication::MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)

{

// Pass mouse messages to the ArcBall so it can build internal matrices

m_ArcBall.HandleMouseMessages( hWnd, msg, wParam, lParam );

switch (msg)

{

case WM_MOUSEWHEEL:

short sr = HIWORD(wParam);

m_fZDist -= (sr/120) * m_fObjectRadius;

break;

}

return CD3DApplication::MsgProc(hWnd, msg, wParam, lParam);

}

Финальная чистка:

HRESULT CMyD3DApplication::DeleteDeviceObjects()

{

if ( m_dwVertexShader != 0xffffffff )

{

m_pd3dDevice->DeleteVertexShader( m_dwVertexShader );

m_dwVertexShader = 0xffffffff;

}

return S_OK;

}

HRESULT CMyD3DApplication::FinalCleanup()

{

SAFE_RELEASE(m_pTexture);

SAFE_RELEASE(m_pIB);

SAFE_RELEASE(m_pVB);

return S_OK;

}

Забыли только проверить, поддерживает ли вообще система шейдеры.

HRESULT CMyD3DApplication::ConfirmDevice( D3DCAPS8* pCaps, DWORD dwBehavior,

D3DFORMAT Format )

{

if( (dwBehavior & D3DCREATE_HARDWARE_VERTEXPROCESSING ) ||

(dwBehavior & D3DCREATE_MIXED_VERTEXPROCESSING ) )

{

if( pCaps->VertexShaderVersion < D3DVS_VERSION(1,1) )

return E_FAIL;

}

return S_OK;

}

Заднее слово по поводу главного файла.

Подключаемые файлы:

#include <math.h>

#include <D3DX8.h>

#include "D3DApp.h"

#include "D3DFile.h"

#include "D3DFont.h"

#include "D3DUtil.h"

#include "DXUtil.h"

#include "shconst.h"

Макрос для мышиного колеса:


#define WM_MOUSEWHEEL 0x020A

Подключаемые библиотеки:

d3dx8.lib dxguid.lib winmm.lib d3d8.lib ddraw.lib d3dxof.lib

Файл shconst.h (какие регистры для чего используются):


#define CLIP_MATRIX 0

#define CLIP_MATRIX_1 1

#define CLIP_MATRIX_2 2

#define CLIP_MATRIX_3 3

#define INVERSE_WORLD_MATRIX 4

#define INVERSE_WORLD_MATRIX_1 5

#define INVERSE_WORLD_MATRIX_2 6

#define EYE_VECTOR 10

#define LIGHT_POSITION 11

#define SPEC_POWER 12

#define DIFFUSE_COLOR 14

#define LIGHT_COLOR 15

Те, кто знал.

⌠Литрбол √ спорт для настоящих мужчин■

Лозунг

Вот, собственно и сама шейдерная программа:

#include "shconst.h"

vs.1.1

; transpose and transform to clip space

mul r0, v0.x, c[CLIP_MATRIX]

mad r0, v0.y, c[CLIP_MATRIX_1], r0

mad r0, v0.z, c[CLIP_MATRIX_2], r0

add oPos, c[CLIP_MATRIX_3], r0

; output texture coords

mov oT0, v7

; transform normal

dp3 r1.x, v3, c[INVERSE_WORLD_MATRIX]

dp3 r1.y, v3, c[INVERSE_WORLD_MATRIX_1]

dp3 r1.z, v3, c[INVERSE_WORLD_MATRIX_2]

; renormalize it

dp3 r1.w, r1, r1

rsq r1.w, r1.w

mul r1, r1, r1.w

; light vector L

; we need L towards the light, thus negate sign

mov r5, -c[LIGHT_POSITION]

; N dot L

dp3 r0.x, r1, r5

; compute normalized half vector H = L + V

add r2, c[EYE_VECTOR], r5 ; L + V

; renormalize H

dp3 r2.w, r2, r2

rsq r2.w, r2.w

mul r2, r2, r2.w

; N dot H

dp3 r0.y, r1, r2

; compute specular and clamp values (lit)

; r0.x - N dot L

; r0.y - N dot H

; r0.w - specular power n

mov r0.w, c[SPEC_POWER].y ; n must be between √128.0 and 128.0

lit r4, r0

mul r1, c[DIFFUSE_COLOR], r4.y

mul r2, c[LIGHT_COLOR], r4.z

add oD0, r1, r2

Строка ⌠vs 1.1■ обозначает то, что программа написана для vertex shader версии 1.1 . Дальше идёт стандартное преобразование точки, т.е. умножение её на мировую, видовую и перспективную матрицу, но по причине того, что эти три матрицы мы помножили заранее (FrameMove), то нам остаётся только помножить нашу точку на полученную ранее матрицу:


mul r0, v0.x, c[CLIP_MATRIX] ; умножение первой строки матрицы на x-компоненту нашей точки, результат записывается во регистр r0

mad r0, v0.y, c[CLIP_MATRIX_1], r0 ; умножение второй строки матрицы на y-компоненту нашей точки, при чём результат прибавляется в уже записанному в r0

mad r0, v0.z, c[CLIP_MATRIX_2], r0 ; тоже самое с третьей строкой матрицы

add oPos, c[CLIP_MATRIX_3], r0 : oPos √ выходной регистр, в который записывается новое положение точки

 

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


mul r0, v0.x, c[CLIP_MATRIX] - делает:

r0.x = c[CLIP_MATRIX][0]*v0.x

r0.y = c[CLIP_MATRIX][1]*v0.y

r0.z = c[CLIP_MATRIX][2]*v0.z

r0.w = c[CLIP_MATRIX][3]*v0.w

mad r0, v0.y, c[CLIP_MATRIX_1], r0 - делает:

r0.x = r0.x + c[CLIP_MATRIX][0]*v0.x

r0.y = r0.y + c[CLIP_MATRIX][1]*v0.y

r0.z = r0.z + c[CLIP_MATRIX][2]*v0.z

r0.w = r0.w + c[CLIP_MATRIX][3]*v0.w

mad r0, v0.y, c[CLIP_MATRIX_1], r0 - делает:

r0.x = r0.x + c[CLIP_MATRIX][0]*v0.x

r0.y = r0.y + c[CLIP_MATRIX][1]*v0.y

r0.z = r0.z + c[CLIP_MATRIX][2]*v0.z

r0.w = r0.w + c[CLIP_MATRIX][3]*v0.w

add oPos, c[CLIP_MATRIX_3], r0

r0.x = r0.x + c[CLIP_MATRIX][0]

r0.y = r0.y + c[CLIP_MATRIX][1]

r0.z = r0.z + c[CLIP_MATRIX][2]

r0.w = r0.w + c[CLIP_MATRIX][3]

(считается, что на w-составляющую можно просто забить)

c[x][y] √ матрица 4х4. Если кто решит, что я перепутал строки и столбцы, то тот будет почти прав. Дело в том, что в ДХ хранятся матрицы транспонированном виде, вероятно для удобства, т.к. в этом случае проще реализовать умножение. Сами посудите, лучше так (если m √ массив из 4*4 подряд идущих элементов):

r0.x = r0.x + m[CLIP_MATRIX*4 + 0]*v0.x

r0.y = r0.y + m[CLIP_MATRIX*4 + 1]*v0.y

r0.z = r0.z + m[CLIP_MATRIX*4 + 2]*v0.z

r0.w = r0.w + m[CLIP_MATRIX*4 + 3]*v0.w

(начальный адрес увеличиваем каждый раз на 1, подряд идущие элементы)

чем:

r0.x = r0.x + m[CLIP_MATRIX + 0*4]*v0.x

r0.y = r0.y + m[CLIP_MATRIX + 1*4]*v0.y

r0.z = r0.z + m[CLIP_MATRIX + 2*4]*v0.z

r0.w = r0.w + m[CLIP_MATRIX + 3*4]*v0.w

(начальный адрес увеличиваем каждый раз на 4, элементы разбросаны по всей матрице)

Координаты текстуры заносятся в выходной регистр oT0 (oT1, oT2, ...):

; output texture coords

mov oT0, v7

Так. Дальше обрабатываем нормали:

; transform normal

dp3 r1.x, v3, c[INVERSE_WORLD_MATRIX]

dp3 r1.y, v3, c[INVERSE_WORLD_MATRIX_1]

dp3 r1.z, v3, c[INVERSE_WORLD_MATRIX_2]

Почему инверсная матрица? А попробуйте использовать не инверсную и сразу поймете, зачем это надо.

Нормализуем вектора нормалей:

; renormalize it

dp3 r1.w, r1, r1 ; в r1.w заносится скалярное произведение вектора r1 на его же самого по трём составляющим, и т.о. получаем квадрат длины вектора (r1.w = L^2 = r1.x*r1.x + r1.y*r1.y + r1.z*r1.z)

rsq r1.w, r1.w ; r1.w = 1/sqrt(r1.w) √ обратный квадратный корень

mul r1, r1, r1.w : умножение составляющих вектора r1 на r1.w

В качестве модели освещения будем использовать модель освещения по Фонгу. В идеале мы имеем формулу:

K*cosn(alpha)

где K √ коэффициент отражения, alpha √ угол между векторами нормали и света, а n √ specular power (величина блика). В нашем случае будем использовать формулу:

Ir = Id * ((N dot L) + K*(R dot V)n)

где Ir √ интенсивность отражения,

Id√ интенсивность падающего света.

N √ нормаль

L √ вектор света

R √ вектор отражённого света относительно нормали n

V √ вектор направления взгляда

Как видно, выражение cosn(alpha) мы заменили на эквивалентное (R dot V)n. Но получить вектор R проблематично, и по этому некий тв. James F. Blinn предложил использовать следующий метод. Берём вектор:

H = (L + V) / 2

и делаем такой вот финт:

(N dot ((L + V) / 2)) n

или:

(N dot H) n

В итоге получаем:

Ir = Id * ((N dot L) + K*(N dot H)n)

Почему это работает, я так и не понял, и признаюсь в этом честно.

Реализовываем всё сказанное. В регистр r5 вносим вектор направления света (координаты источника, взятые с противоположным знаком √ как будто свет идёт из бесконечно удаленной точки, направлен в нулевую точку и имеет это самое направление)

; light vector L

; we need L towards the light, thus negate sign

mov r5, -c[LIGHT_POSITION]

Находим скалярное произведение вектора света и нормали по трём составляющим:

; N dot L

dp3 r0.x, r1, r5

В r2 записываем сумму векторов направления взгляда и света:

; compute normalized half vector H = L + V

add r2, c[EYE_VECTOR], r5 ; L + V

Дальше должно быть всё понятно:

; renormalize H

dp3 r2.w, r2, r2

rsq r2.w, r2.w

mul r2, r2, r2.w

; N dot H

dp3 r0.y, r1, r2

В r0.w записываем n:

mov r0.w, c[SPEC_POWER].y ; n must be between √128.0 and 128.0

Таким образом мы получаем регистр r0 у которого:

r0.x = N dot L

r0.y = N dot H

r0.w = n

Конечный расчёт светового коэффициента:

lit r4, r0

Под этой строчкой скрывается следующее:

dest.x = 1;

dest.y = 0;

dest.z = 0;

dest.w = 1;

float power = src.w;

const float MAXPOWER = 127.9961f;

if (power < -MAXPOWER)

power = -MAXPOWER; // Fits into 8.8 fixed point format

else if (power > MAXPOWER)

power = -MAXPOWER; // Fits into 8.8 fixed point format

if (src.x > 0)

{

dest.y = src.x;

if (src.y > 0)

{

// Allowed approximation is EXP(power * LOG(src.y))

dest.z = (float)(pow(src.y, power));

}

}

И, наконец, сам цвет вычисляется и записывается в выходной регистр цвета oD0. mul r1, c[DIFFUSE_COLOR], r4.y

mul r2, c[LIGHT_COLOR], r4.z

add oD0, r1, r2

Есть ещё регистры цвета (oD0, oD1 и т.д.), но почему-то у меня с ними ничего не работало. Вообще, по идеи, последние строки могут выглядеть и так:

mul oD0, c[DIFFUSE_COLOR], r4.y

mul oD1, c[LIGHT_COLOR], r4.z

Вот всё, что я хотел сказать.

Слово на после.

⌠Партия Ленина √ сила народная

нас к торжеству коммунизма ведёт■

Гимн СССР

Сразу оговорюсь, что пример взят почти полностью с www.gamedev.net, а также при составлении статьи я опирался на неё же. Некоторые выводы я делал сам, и некоторые вещи могут содержать неточности (читать √ ошибки, я всё ж тоже человек), так что если что, сразу приношу свои извинения. Если у кого будут какие комментарии, смело пишите. Похвальные письма тоже можете присылать. Чуть позже я напишу, как сделать почти тоже самое с помощью Cg. Кое-что я взял с http://directxdesign.narod.ru/papers.htm .

Относительно пиксельных шейдеров, с этим труднее. Пока видеокарты, которые их поддерживают, распространены плохо, а на софтварном уровне их никто делать не будет √ это вам не вертексы, которых раз-два и обчелся, здесь надо обрабатывать каждый пиксель. Но темнее менее про них тоже можно сказать много интересного.

Белов Анатолий.

anatol@sstu.ru




Послесловие.

      Т.к. сегодня вышел выпуск, который был написан уже очень давно, то и заключительное слово, пожалуй, следует оставить для следующих наших встреч. Не буду вам напоминать, что свои вопросы и пожелания, критику и благодарности или просто свои размышления - все направляйте в адрес gamemaker@pisem.net. Будем очень рады.
      И напоследок позволю себе небольшое объявление. Всем, кто хотел бы принять участие в наполнении и поддержки сайта GameMaker.ru - милости просим. Если вы, конечно, чувствуете, что тема создания игр вам интересна. Другими словами все, кто хотел бы принимать участие в возрождении сайта, пишите на gamemaker@pisem.net. Особенно приветствуется знание PHP/Perl и умение рисовать. (Хотя это везде так:) )
PS: Выпуск 17 уже готов и выйдет через несколько дней.

     До скорой встречи!


SlyMagic.

"Как самому создать компьютерную игру" (с) 2002
Использование любых материалов рассылки возможно только с разрешения автора.
Тираж 7300 экземпляров.


http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное