Visual Basic - мастерская разработчика
Библиотеки

DirectX

Обзоры
DirectDraw
Direct3D
DirectX Audio
DirectPlay
DirectInput
Fido Topics
SourceCode
Tools&Libs

OpenGL

Статьи и учебники
Fido Topics
SourceCode
Tools&Libs

Архив по Glide

Движки

Обзоры
Учебники
SourceCode
Downloads

Создание игр

Ваши игры

Обзорные статьи
Учебники
Fido Topics
SourceCode
Download

Stuff

Программер-Чат

Псевдо-FTP
Disclaimer
Оффтопик

 

DirectX Graphics - начнем снова

Часть 4- Добавляем освещение

[Предыдущая часть] [Следующая часть] [Приложения к этой части]

Приветствую вас в четвертой части учебника DirectX Graphics!
В этой части мы будем заниматься добавлением источника освещения в нашу сцену. Освещение в DirectX Graphics может быть различных типов: вещественный свет, направленный свет, точечный свет... Подробнее я на этом остановлюсь в какой-нибудь теоретической статье, а сеячас скажу только, что в этой части мы будем использовать направленный свет (Directional lighting). У направленного света источник находится в начале координат, и еще есть направление. Оно указывается конкретной 3D-координатой, программно - объектом D3DXVECTOR3. В эту точку свет и светит (уж простите за каламбур :) Свет так же имеет расстояние, на которое он распространяется.

Может вы помните игры, которые были давно и под ДОС? Там тоже было примитивнейшее освещение, рассчитываемое вручную. Так вот, теперь нам не надо этим заниматься! Рассчет освещения берет на себя сам Direct3D. Это хорошо, так как формулы освещения громоздки и у любого начинающего вызвали бы естесственное отторжение при первом же взгляде.

Вобщем, теоретически я вас загрузил, теперь переходим к практике.
Создайте новый Win32-проект и подключите к нему lib'ы d3dx8.lib, d3d8.lib и winmm.lib К проекту добавьте пустой файл dgtut4.cpp и начинаем писать программу!

#include <d3dx8.h>
#include <mmsystem.h>

LPDIRECT3D8 g_pD3D = NULL; // Объект Direct3D
LPDIRECT3DDEVICE8 g_pd3dDevice = NULL; //Устройство рендеринга
LPDIRECT3DVERTEXBUFFER8 g_pVB = NULL; //Буфер для вершин

//Структура, определяющая наш формат вершины (aka FVF)
struct CUSTOMVERTEX
{
D3DXVECTOR3 position; // 3D-позиция вершины
D3DXVECTOR3 normal; // Нормаль поверхности для вершины
};

//Флаговое описание нашего FVF
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_NORMAL)

VOID Cleanup();
HRESULT InitD3D( HWND hWnd );
HRESULT InitGeometry();
VOID SetupLights();
VOID SetupMatrices();
VOID Render();

В структуре вершины произошли изменения. Во-первых, мы убрали определение цвета вершины. Мы не будем использовать цвет вершины, так как переходим к использованию материалов. Материал - это такой объект, который задается примитиву и определяющий как будет отражаться от него свет. Если помните школьную физику, то там понятие материала выглядело примерно таким же образом. В этой части учебника мы создадим материал, который будет отражать желтый свет.
Во-вторых, мы ввели понятие нормали. Нормаль будет нужны в этой части для внутренних операций Direct3D со светом. Пара слов о том, что такое нормаль:

Каждая поверхность в объекте имеет перпендикулярный поверхности вектор - нормаль. Направление перпендикуляра определяется порядком, в котором заданы вершины поверхности. Этот порядок определяет переднюю и заднюю стороны поверхности. Вы можете почитать об этом в статье "Основы трехмерной графики". Естественно, что нормаль выходит из передней стороны поверхности. Задняя сторона как правило не отрисовывается. Так вот, нормали выходящие из поверхностей называются поверхностными нормалями (ну или нормалями поверхностей). Вам не надо указывать их, так как система сама вычисляет их по мере необходимости. Существует так же другой тип нормалей - нормали вершин. Это нормаль, которая выходит из вершины.

Нормаль вершины используется при вычислении затенения примитива по методу Гуро, а так же при расчете освещения примитива и текстурных эффектов. Давайте разберемся в чем тут дело. На картинке вы можете видеть две поверхности повернутые к вам боком S1 и S2.

У них есть нормали поверхностей Ns1 и Ns2. Есть так же нормаль вершины Nv. Вы видите, что углы между нормалями поверхностей и нормалью вершины одинаковые. Это значит, что Direct3D при наложении световых эффектов будет брать этот угол и равномерно интерполировать цвет вершины как на поверхность S1, так и на поверхность S2. При этом получается плавный переход света через вершину из которой выходит нормаль.
На другой картинке, углы между нормалями не одинаковые. Это значит, что одна из сторон получит больше света и цвет вершины будет неравномерно интерполироваться на остальные пиксели поверхностей.

В кратце, вот для этого и нужны нормали. Ну как вам мои пара слов? :) Ладно, давайте писать программу дальше

LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
switch( msg )
{
case WM_DESTROY:
PostQuitMessage( 0 );
return 0;
}

return DefWindowProc( hWnd, msg, wParam, lParam );
}

 

INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, INT )
{
//Зарегистрировать класс окна
WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L,
GetModuleHandle(NULL), NULL, NULL, NULL, NULL,
"D3D Tutorial", NULL };
RegisterClassEx( &wc );

// Создать окно
HWND hWnd = CreateWindow( "D3D Tutorial", "D3D Tutorial 04: Lights",
WS_OVERLAPPEDWINDOW, 100, 100, 300, 300,
GetDesktopWindow(), NULL, wc.hInstance, NULL );

// Инициализировать Direct3D
if( SUCCEEDED( InitD3D( hWnd ) ) )
{
//Создать буфер вершин
if( SUCCEEDED( InitGeometry() ) )
{
// Показать окно
ShowWindow( hWnd, SW_SHOWDEFAULT );
UpdateWindow( hWnd );

// Войти в главный цикл сообщений
MSG msg;
ZeroMemory( &msg, sizeof(msg) );
while( msg.message!=WM_QUIT )
{
if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
else
Render();
}
}
}

// Убить все и выйти
Cleanup();
UnregisterClass( "D3D Tutorial", wc.hInstance );
return 0;
}

HRESULT InitD3D( HWND hWnd )
{
// Создать объект D3D, который потребуется для создания устройства рендеринга
if( NULL == ( g_pD3D = Direct3DCreate8( D3D_SDK_VERSION ) ) )
return E_FAIL;

// Получить текущий режим экрана
D3DDISPLAYMODE d3ddm;
if( FAILED( g_pD3D->GetAdapterDisplayMode( D3DADAPTER_DEFAULT, &d3ddm ) ) )
return E_FAIL;

D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory( &d3dpp, sizeof(d3dpp) );
d3dpp.Windowed = TRUE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferFormat = d3ddm.Format;
d3dpp.EnableAutoDepthStencil = TRUE;
d3dpp.AutoDepthStencilFormat = D3DFMT_D16;

Мы будем работать с буфером глубины, поэтому добавили к структуре d3dpp две строчки с определениями формата этого буфера.

if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp, &g_pd3dDevice ) ) )
{
return E_FAIL;
}

// Убрать culling так, чтобы мы видели и переднюю и заднюю сторону треуголника
g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );

// Включить Z-буфер
g_pd3dDevice->SetRenderState( D3DRS_ZENABLE, TRUE );

// Все в порядке, можно вернуть что-нибудь хорошее
return S_OK;
}

 

VOID Cleanup()
{
if( g_pVB != NULL )
g_pVB->Release();

if( g_pd3dDevice != NULL)
g_pd3dDevice->Release();

if( g_pD3D != NULL)
g_pD3D->Release();
}

Здесь отличие только в том, что в самом конце InitD3D мы включаем Z-буфер. Как вы наверное знаете, Z-буфер - это вспомогательная поверхности, в которой вместо информации о пикселях хранится информация о расстоянии точки от обозревателя. Это помогает правильно и быстро отображать перекрывающиеся геометрические фигуры.

Функция InitGeometry претерпела большие изменения:

HRESULT InitGeometry()
{
// Создать буфер вершин
if( FAILED( g_pd3dDevice->CreateVertexBuffer( 50*2*sizeof(CUSTOMVERTEX),
0, D3DFVF_CUSTOMVERTEX,
D3DPOOL_DEFAULT, &g_pVB ) ) )
{
return E_FAIL;
}

Мы сразу создаем буфер под 100 вершин.

// Создать по вершинам цилиндр. Мы так же указываем нормали
CUSTOMVERTEX* pVertices;
if( FAILED( g_pVB->Lock( 0, 0, (BYTE**)&pVertices, 0 ) ) )
return E_FAIL;
for( DWORD i=0; i<50; i++ )
{
FLOAT theta = (2*D3DX_PI*i)/(50-1);
pVertices[2*i+0].position = D3DXVECTOR3( sinf(theta),-1.0f, cosf(theta) );
pVertices[2*i+0].normal = D3DXVECTOR3( sinf(theta), 0.0f, cosf(theta) );
pVertices[2*i+1].position = D3DXVECTOR3( sinf(theta), 1.0f, cosf(theta) );
pVertices[2*i+1].normal = D3DXVECTOR3( sinf(theta), 0.0f, cosf(theta) );
}
g_pVB->Unlock();

return S_OK;
}

После этой процедуры, мы получим буфер вершин, задающих цилиндр. Теперь, задаем матрицы, чтобы видеть наш мир в движении:

VOID SetupMatrices()
{
// Установить матрицу мира
D3DXMATRIX matWorld;
D3DXMatrixIdentity( &matWorld );
D3DXMatrixRotationX( &matWorld, timeGetTime()/500.0f );
g_pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld );

Функция D3DXMatrixIdentity создает единичную матрицу, главная диагональ которой установлена в 1, а все остальное в 0. Это примечательно тем, что не дает абсолютно никакой трансформации объектам, к которым применена матрица. На основе единичной матрицы затем создаются трансформации, например, как в нашем случае, вращение по оси X.

// Установить матрицу обзора
D3DXMATRIX matView;
D3DXMatrixLookAtLH( &matView, &D3DXVECTOR3( 0.0f, 3.0f,-5.0f ),
&D3DXVECTOR3( 0.0f, 0.0f, 0.0f ),
&D3DXVECTOR3( 0.0f, 1.0f, 0.0f ) );
g_pd3dDevice->SetTransform( D3DTS_VIEW, &matView );

// Установить матрицу проекции
D3DXMATRIX matProj;
D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/4, 1.0f, 1.0f, 100.0f );
g_pd3dDevice->SetTransform( D3DTS_PROJECTION, &matProj );
}

Рендерим:

VOID Render()
{
// Очистить задний буфер и z-буфер в синий цвет
g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );

Теперь, мы очищаем так же и Z-буфер.

// Начало рендеринга сцены
g_pd3dDevice->BeginScene();

Установим освещение и материал объектов:

// Установить освещение и материал
SetupLights();

// Установить матрицы мира, обзора и проекции
SetupMatrices();

// Рендерить содержимое буфера вершин
g_pd3dDevice->SetStreamSource( 0, g_pVB, sizeof(CUSTOMVERTEX) );
g_pd3dDevice->SetVertexShader( D3DFVF_CUSTOMVERTEX );
g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2*50-2 );

Обратите внимание, что теперь мы рендерим не набор треугольников (TRIANGLELIST), а ленту треугольников (TRIANGLESTRIP). Более подробно о лентах и веерах треугольников читайте в теоретической статье, которая должна быть где-то вокруг.

//Конец рендеринга
g_pd3dDevice->EndScene();

//Показать содержимое заднего буфера на экране
g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
}

Вот она, функция SetupLights, которая осветит нам путь в темном месте :)

VOID SetupLights()
{
//Создать и назначить объекту материал желтого цвета
D3DMATERIAL8 mtrl;
ZeroMemory( &mtrl, sizeof(D3DMATERIAL8) );
mtrl.Diffuse.r = mtrl.Ambient.r = 1.0f;
mtrl.Diffuse.g = mtrl.Ambient.g = 1.0f;
mtrl.Diffuse.b = mtrl.Ambient.b = 0.0f;
mtrl.Diffuse.a = mtrl.Ambient.a = 1.0f;
g_pd3dDevice->SetMaterial( &mtrl );

Как я уже объяснял, материал задает то, как свет будет отражаться от объекта. После вызова g_pd3dDevice->SetMaterial каждый примитив будет рендериться с помощью указанного материала вплоть до следующего вызова этого метода.

//Установить белый направленный свет с осциллирующием направлением
D3DXVECTOR3 vecDir;
D3DLIGHT8 light;
ZeroMemory( &light, sizeof(D3DLIGHT8) );
light.Type = D3DLIGHT_DIRECTIONAL;
light.Diffuse.r = 1.0f;
light.Diffuse.g = 1.0f;
light.Diffuse.b = 1.0f;

Здесь мы задали параметры структуры D3DLIGHT8. Вы можете видеть, что мы задали направленный свет. Затем, мы задали рассеивающий цвет света.

vecDir = D3DXVECTOR3(cosf(timeGetTime()/350.0f),
1.0f,
sinf(timeGetTime()/350.0f) );
D3DXVec3Normalize( (D3DXVECTOR3*)&light.Direction, &vecDir );
light.Range = 1000.0f;

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

g_pd3dDevice->SetLight( 0, &light );

Эта операция назначает свет устройству рендеринга. Первый нолик в параметрах означает индекс, под каким будет назначен свет. Если такому индексу уже был назначен свет, то он перезаписывается. В результате получается своеобразная база данных различных "светильников" :)

g_pd3dDevice->LightEnable( 0, TRUE );

Теперь мы говорим устройству рендеринга, что свет под индексом 0 включен и его надо рендерить.

g_pd3dDevice->SetRenderState( D3DRS_LIGHTING, TRUE );

Сия строчка разрешает рендеринг света.

//Установить некоторый вещественный свет
g_pd3dDevice->SetRenderState( D3DRS_AMBIENT, 0x00202020 );
}

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

На этом вроде как и все. В следующей части мы будем загружать текстуры из файла и натягивать их на созданные объекты.

Приложение: Готовый проект (22k)

Приятного программирования: Antiloop

[Вверх]

Posted: 23.01.2k1
Autor: Antiloop
<anti_loop@mail.ru>

 


Проект
Создание Народного Учебника по OpenGL

Участвовать!
Поиск
Найдите статью или файл:


Рассылка
Новости сайта
La Vision в вашем почтовом ящике








Программирование на С++ Delphi и Паскаль
Центр демо-искусства в России