← Декабрь 2002 → | ||||||
1
|
||||||
---|---|---|---|---|---|---|
2
|
3
|
4
|
5
|
6
|
7
|
8
|
9
|
10
|
11
|
12
|
13
|
14
|
15
|
16
|
18
|
19
|
20
|
21
|
22
|
|
24
|
25
|
26
|
27
|
28
|
29
|
|
30
|
31
|
За последние 60 дней ни разу не выходила
Открыта:
25-10-2000
Адрес
автора: comp.soft.prog.gamemaker-owner@subscribe.ru
Статистика
0 за неделю
Как самому создать компьютерную игру #16
Информационный Канал Subscribe.Ru |
Адрес для обратной связи - gamemaker@pisem.net 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] - входные регистры, количество которых зависит от конкретной команды.Основные команды:
Макрокоманды:
Другие команды
Что касается переменных (ресурсов):
Картинка (для наших маленьких читателей J ):Допустим вы рендируете модель с помощью DrawIndexPrimitive. Модель состоит из n-ого числа вертексов. Если вы рендируете с помощью обычных средств, то сначала надо задать по отдельности матрицу вида, проекции и мировую, затем задать свет и теперь рендировать. Обработка каждой вершины пойдёт по заранее запланированной программе. Теперь давайте посмотрим, как пойдёт дело при использовании ВШ.Простейший ВШ: vs.1.1 ; версия ВШ dp4 oPos.y, v0, c1 ; oPos - выходной регистр позиции, т.е. позиция вертекса в 3Д dp4 oPos.z, v0, c2 mov oD0, c4 ; oD0-выходной регистр цвета. Сюда заносится цвет вертекса Теперь что где что значит: v0 √ первоначальный вертекс (вектор {x,y,z,w}). Входной регистр.c[0,1,2,3] √ константные регистры, в которых содержится матрица преобразования.c4 - константный регистр. Цвет.Уточнение: константные регистры задаются пользователем. Т.о. Каждый вертекс проходит заданную именно нами обработку, прежде чем попасть на растеризацию. Такой подход обеспечивает большую гибкость и масштабируемость средств визуализации. Подробнее о работе. ⌠Только упорный труд может сделать из обезьяны слесаря 6-ого разряда■ из Фоменковских афоризмов Давайте, штоль, попробуем что-то сделать? Я, пожалую, не буду изощряться, и придумывать что-то новое, а возьму пример из www.Gamedev.net . В этом примере будем крутить выпуклый квадрат, и там ещё будет анизотропное освещение.Чтоб упростить задачу предлагаю использовать Микрософтный прибамбас для таких вещей, именуемый как класс CD3Dapplication. Для тех, кто не знаком с этим достижением мысли инженеров известной конторы, поясню. Это, так сказать, базовый набор функций для любого приложения (нечто типа MFC для создания приложений типа компьютерных игр). Этот класс создаёт главное окно, реализует обработку виндоусовских мессаг, и вызывает необходимый набор функций, в которые входят:Вот так. Т.о. чтоб всё это использовать, надо создать свой класс, наследуемый от 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 vertexFLOAT nx, ny, nz; // the normalFLOAT 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; } Чтоб использовать шейдеры, их надо загрузить. Тут есть есть два пути:
Я пошёл вторым путём (так сложнее и правильнее?). Если кто хочет поступить в соответствии с первым вариантом, могут сделать так: 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■. Думаю если кто об этом не знал, всё понял.Откомпилили. Теперь дальше. Грузим:
{ // 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 processHANDLE hFile, hMap; // handle file and handle mapped fileTCHAR tempVSPath[512]; // temporary file pathHRESULT hr; // errorif( 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 spaceD3DVSD_REG(3, D3DVSDT_FLOAT3 ), // normalsD3DVSD_REG(7, D3DVSDT_FLOAT2), // tex coordD3DVSD_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);
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"Макрос для мышиного колеса:
Подключаемые библиотеки:d3dx8.lib dxguid.lib winmm.lib d3d8.lib ddraw.lib d3dxof.lib Файл shconst.h (какие регистры для чего используются):
#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), то нам остаётся только помножить нашу точку на полученную ранее матрицу:
mad r0, v0.y, c[CLIP_MATRIX_1], r 0 ; умножение второй строки матрицы на y-компоненту нашей точки, при чём результат прибавляется в уже записанному в r0mad r0, v0.z, c[CLIP_MATRIX_2], r 0 ; тоже самое с третьей строкой матрицыadd oPos, c[CLIP_MATRIX_3], r0 : oPos √ выходной регистр, в который записывается новое положение точки
Кто не понЯл проделанную операцию, советую взять элементарный учебник по линейной алгебре и аналитической геометрии и там прочитать главу относительно перемножения матриц. Для тех, кому лень это делать, показываю:
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 √ вектор отражённого света относительно нормали nV √ вектор направления взглядаКак видно, выражение 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.ymul 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 .Относительно пиксельных шейдеров, с этим труднее. Пока видеокарты, которые их поддерживают , распространены плохо, а на софтварном уровне их никто делать не будет √ это вам не вертексы, которых раз-два и обчелся, здесь надо обрабатывать каждый пиксель. Но темнее менее про них тоже можно сказать много интересного.Белов Анатолий. Т.к. сегодня вышел выпуск, который был написан уже очень давно, то и заключительное слово, пожалуй, следует оставить для следующих наших встреч. Не буду вам напоминать, что свои вопросы и пожелания, критику и благодарности или просто свои размышления - все направляйте в адрес gamemaker@pisem.net. Будем очень рады. И напоследок позволю себе небольшое объявление. Всем, кто хотел бы принять участие в наполнении и поддержки сайта GameMaker.ru - милости просим. Если вы, конечно, чувствуете, что тема создания игр вам интересна. Другими словами все, кто хотел бы принимать участие в возрождении сайта, пишите на gamemaker@pisem.net. Особенно приветствуется знание PHP/Perl и умение рисовать. (Хотя это везде так:) ) PS: Выпуск 17 уже готов и выйдет через несколько дней. До скорой встречи! SlyMagic.
Использование любых материалов рассылки возможно только с разрешения автора. Тираж 7300 экземпляров. |
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||