ЙойЕаЪа, программеры! Вы
ведь слышали о релизе крупнотвердым
восьмого дыгса? Вот о нем я сегодня и
расскажу. Выход его стоил мне, как и
большинству остальных дыгз программерам
кучи лишней работы. Среди этого шлака можно
разглядеть такую коляску, как прочтение
заново около 750 страниц чистого буржуйского.
После осмысливания небольшой части оного,
той части, где рассказывается про новую
архитектуру и прочем шланге, в голову
приходит мысль о том, что на этот раз
маздайцы действительно хорошо постарались.
Конечно, хвалить их – великий грех,
замолить который можно только поеданием
пятидесяти тонн мюсли с червями в
собственном соку, но, тем не менее, сегодня я
маздайских программеров похвалю (50 тонн
мюсли и червей мне! 🙂 ). Так же в глаза
бросается оптимизация (вот уж чего не
ожидал ). Не веришь? Вот ставь себе обратно
дх7 и запускай dxdiag (Пуск->выполнить “dxdiag”
). Протестируй себе всю графику. Запомнил
примерную скорость? Вот теперь ставь
восьмой, и опять запускай dxdiag. ЧУВСТВУЕШЬ
РАЗНИЦУ? То-то же.
Что новенького?
Разумеется, ты читал
новость о фишках дх8. Там я основные
графиковские написал (хочешь подробнее, -
сдк тебе в руки, мне здесь его 3/4 переводить
в лом, да и не нужно в общем-то). В
продолжение той темы напишу, что
инициализировать д3д стало НАСТОЛЬКО проще,
что я, когда прочитал, чуть гирю себе на
бошку не выронил (вопрос: что делала гиря у
меня над головой? Первый чувак, который
правильно на него ответит, получит в
подарок от меня пароль к ящику krome_mena@debilov.net).
Тебе больше не придется возиться с дровами,
поверхностями и прочим грунтом.
Естественно, если захочешь – фраг тебе в
руки. Всякие з-буферы, цепи буферизации и
тому подобное содержатся ВНУТРИ интерфейса
IDirect3DDevice8, и задаются при его создании. Вот
почему в этой статье я сразу же покажу тебе,
как создавать з-буфер (хы-хы, добавим флаг –
и всех делов!). Ну, туристы, в путь!
Приготовьте клаву,
товарисчи!
Да, пожалуй, приготовь
клаву, мы сейчас будем кодить. По ходу всего
представления я буду показывать новые
фишки дгз8. Старый Хакер3D нам больше не
понадобится (хотя при желании ты можешь
затереть АБСОЛЮТНО ВСЕ содержимое файла
main.cpp (или как ты его там назвал ;-))) ) ),
поэтому создавай новый проект, File->New, тип:
Win32 Application. Называй его как душе угодно, я,
например, решил название не менять, и у меня
он по-прежнему называется Xakep3D. Вот у тебя
есть пустой проект. Добавляй туда файло (
опять File->New ) C++ Source File. Опять же, называй
его, что в голову взбрендит ( main.cpp ). Вот
теперь готовь клаву, протри пыль на ней,
разомни пальцы и слушай далее. По традиции
все начинается с подключения заголовков.
Нам нужно их только два: d3dx8.h и mmsystem.h. Зачем
ммсустем? Там объявлена функция timeGetTime(),
которая пригодится позже. д3дгз8 –
заголовок библиотеки D3DX, которая нам
пригодится. Идем дальше. Объявляем
интерфейсы IDirect3D8, IDirect3DDevice8 и IDirect3DVertexBuffer8.
Вертех Буфферь нужен для хранения данных
вершин. Остальные, я думаю, в объяснении не
нуждаются. Затем надо объявит тип –
структуру, в которой будут храниться
параметры вершин, а именно: координаты и
цвет. Дело в том, что для удобства из д3д были
убраны все стандартные вертексы, и теперь
их надо создавать самому, что действительно
удобно. Для простоты, я назову эту структуру
XAKEPVERTEX. Первыми параметрами должны быть (
обязательно, ведь откуда д3д знать имена
внутренних переменных структуры, тем более,
что она уже закомпилена, и, если ты не знал,
обращения к переменным – членам структуры
происходит не по их именам, а по смещению
адреса в памяти, который мы и передаем д3д )
координаты, а затем – цвет ( йоу, мазафака! ).
Теперь листинг.
#include <d3dx8.h> //Д3дшный
заголовок
#include <mmsystem.h> //Виндошный заголовок, из
него нужна только ф-я timeGetTime
LPDIRECT3D8 lpD3D; //Интерфейс д3д
LPDIRECT3DDEVICE8 lpD3DD; //Интерфейс д3д девайса
LPDIRECT3DVERTEXBUFFER8 lpVB; //Интерфейс буфера вершин
struct XAKEPVERTEX //Объявление структуры, которая
хранит параметры вершины
{
FLOAT x, y, z; // Координаты
DWORD color; // Цвет
};
Конец листинга 1.Все
внимание сюда! Сейчас будет инициализация,
короткая до безобразия. Возьми гирю в руки,
и подними ее над головой ( я тебе пытаюсь
намекнуть правильный ответ 😉 ). Функция
инициализации будет называться просто
Initialize. Параметр один – HWND hWnd. Сначала надо
создать интерфейс IDirect3D8. Для этого
существует специальная функция Direct3DCreate8,
которая имеет один параметр – версия сдк (
D3D_SDK_VERSION ) и возвращает адрес
свежесозданного интерфейса. Следующим
шагом получим текущий режим экрана, так как
он у тебя теоретически всегда ( исключением
является у меня 1280х1024х24; вместе с окошками
фурычит, а в д3д – нет 🙁 ) будет работать. Для
этого существует структура D3DDISPLAYMODE ( d3ddm ), а,
чтобы получить этот режим есть метод IDirect3D8::
GetAdapterDisplayMode. Его параметры – адаптер и
адрес структуры, которую следует заполнить.
У тебя ведь нет до хренища видеокарт и
мониторов, поэтому ты здесь поставишь
дефолтный адаптер. Адрес структуры – сам
знаешь какой. Теперь у тебя есть все нужное,
для того, чтобы заполнить структуру
D3DPRESENT_PARAMETERS, необходимую для создания
интерфейса девайса. Сначала надо ее
обнулить. Затем, в любом порядке тебе
потребуется указать .Windowed = FALSE ( твоя прога
не в окне ); .SwapEffect = D3DSWAPEFFECT_DISCARD ( сам
определит тип свопирования ); .BackBufferWidth =
d3ddm.Width ( ширина внеэкранного буфера ); .BackBufferWidth
= d3ddm.Width ( высота аналогично ); .BackBufferFormat =
d3ddm.Format ( формат ); .EnableAutoDepthStencil = TRUE ( ты
хочешь геморройной возни с поверхностями
при создании з-буфера? Я – неа. Пусть
сделает все автоматом ); .AutoDepthStencilFormat = D3DFMT_D16
(формат з-буфера, 16 бит на пиксель). Для
красоты картины измени размер окна в
соответствии с полученным разрешением
экрана. Просто посмотри, как это сделал я в
листинге ( HWND_TOPMOST – поверх всех окон ). Вот
теперь, собственно, создание девайса. Все
это по старинке делается методом
IDirect3D8::CreateDevice
(
UINT Adapter, // Адаптер, опять же, дефолтовый
D3DDEVTYPE DeviceType, //Тип девайса. D3DDEVTYPE_HAL, _REF, _SW
HWND hFocusWindow, //Окно
DWORD BehaviorFlags,
//Флаги. Тебе нужен
только D3DCREATE_SOFTWARE_VERTEXPROCESSING
D3DPRESENT_PARAMETERS* pPresentationParameters,
//Параметры девайса, засунь сюда только что
заполненную структуру
IDirect3DDevice8** ppReturnedDeviceInterface //Гыгы, здесь адрес
девайса
);
Дальше запрети обрезание
(гы) обратной стороны полигона и освещение
– все равно у тебя ламп здесь нет. И,
напоследок, поставь камеру в правильное
место. Для этого есть d3dx’овская
вспомогательная функция D3DXMatrixLookAtLH, которая
заполняет матрицу, идущую в качестве
первого параметра, в соответствии с
координатами положения камеры, точки, на
которую она смотрит, и вектора “верх”,
являющимися соответственно вторым, третьим
и четвертым параметрами. И просто
трансформируй вид в соответствии с
полученной матрицей. Еще нужно создать
матрицу проекции. Опять же в этом деле тебе
поможет функция D3DXMatrixPerspectiveFovLH. В ней первый
параметр – указатель на матрицу, которая
будет заполнена, второй – площадь вида в
радианах ( что это такое, посмотри в сдк, мне
сейчас здесь выписывать еще страницу как-то
в лом, тем более там ты лучше поймешь, если,
конечно, инглиш знаешь ), третий – какой-то
там аспект, фактически – на сколько сцена
будет сжата вглубь ( поэкспериментируй –
увидишь ), четвертый – глубина ближней
плоскости проекции, пятый – то же самое по
отношению к дальней плоскости проекции (
гыгы, поставь после прочтения этой статьи
сюда значение 9, позабавляйся ).
HRESULT Initialize( HWND hWnd )
{
// Создание интерфейса д3д
if( NULL == ( lpD3D = Direct3DCreate8( D3D_SDK_VERSION ) ) )
return E_FAIL;
// Получение текущего режима экрана
D3DDISPLAYMODE d3ddm;
if( FAILED( lpD3D->GetAdapterDisplayMode( D3DADAPTER_DEFAULT, &d3ddm ) )
)
return E_FAIL;
// Параметры создаваемого д3д девайса
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory( &d3dpp, sizeof(d3dpp) );
d3dpp.Windowed = FALSE; //В окошке? Фигушки!
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; //Тип смены
изображений между внеэкранным буфером и
экранным
d3dpp.BackBufferWidth = d3ddm.Width; //Ширина внеэкранного
буфера
d3dpp.BackBufferHeight = d3ddm.Height; //Высота ...
d3dpp.BackBufferFormat = d3ddm.Format; //Формат ...
d3dpp.EnableAutoDepthStencil = TRUE; //Автоматический буфер
глубины
d3dpp.AutoDepthStencilFormat = D3DFMT_D16; //Формат оного
//Установка позиции и размера окна проги в
соответствии с полученными
параметрами режима экрана
SetWindowPos( hWnd, HWND_TOPMOST, 0, 0, d3ddm.Width, d3ddm.Height, NULL );
// Создание девайса
if( FAILED( lpD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,&d3dpp, &lpD3DD ) ) )
return E_FAIL;
//Заднюю сторону у полигона не обрезать
lpD3DD->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
//А зачем свет, если его нет?
lpD3DD->SetRenderState( D3DRS_LIGHTING, FALSE );
//Положение камеры
D3DXMATRIX View;
D3DXMatrixLookAtLH( &View, &D3DXVECTOR3( 0.0f, 3.0f, 8.0f ),
&D3DXVECTOR3( 0.0f, 0.0f, 0.0f ),
&D3DXVECTOR3( 0.0f, 1.0f, 0.0f ) );
lpD3DD->SetTransform( D3DTS_VIEW, &View );
//Заполнение матрицы проекции
D3DXMATRIX Proj;
D3DXMatrixPerspectiveFovLH( &Proj, D3DX_PI/4, 1.0f, 1.0f, 12.0f );
lpD3DD->SetTransform( D3DTS_PROJECTION, &Proj );
return S_OK;
}
Вот завершилась
инициализация. Ты видишь, что в ней нет
ничего, что каким либо боком относилось бы к
заданию вершин полигонов. Просто я это
вынес в отдельную функцию. По традиции
переменная с вершинами будет называться vtx.
Всего вершин – 6, так как у тебя будет два
полигона ( для демонстрации з-буфера ). Их
цвета и координаты посмотри в листинге. Для
того чтобы д3д зарендерил эти полигоны,
нужно вершины оных занести в вертехбуфер.
Для создания коего тебе придется
воспользоваться методом
IDirect3DDevice8::CreateVertexBuffer
(
UINT Length, //Размер всех вершин памяти,
DWORD Usage, //Флаги,
DWORD FVF, //Флиги формата структуры,
описывающей вершины,
D3DPOOL Pool, //Тип памяти, в которой будет
храниться весь гумус,
IDirect3DVertexBuffer8** ppVertexBuffer //Адрес получаемого
интерфейса,
);
Созданный буфер вершин у
тебя сейчас пустой. Его надо заполнить. Для
этого существует метод
IDirect3DVertexBuffer8::Lock
(
UINT OffsetToLock, //Смещение адреса
UINT SizeToLock, //Размер, который потребуется для
заполнения
BYTE** ppbData, //Возвращаемый адрес
DWORD Flags //Флаги
);
Теперь при помощи функции
memcpy скопируй содержимое vtx в полученный
адрес. И, наконец, открой вертехбуфер
методом IDirect3DVertexBuffer8::Unlock. Гы, вот ты теперь
готов рендерить.
HRESULT SetVertices()
{
//Параметры вершин полигонов
XAKEPVERTEX vtx[] =
{
{ -0.6f,-2.0f, 1.0f, 0xffff00, },
{ 0.6f,-2.0f, 1.0f, 0x00ffff, },
{ 1.3f, 2.0f, 1.0f, 0x00ff00, },
{ -0.6f,-2.0f, -1.0f, 0xff0000, },
{ 0.6f,-2.0f, -1.0f, 0x0000ff, },
{ 1.3f, 2.0f, -1.0f, 0x00ff00, },
};
//Создание буфера вершин
if( FAILED( lpD3DD->CreateVertexBuffer( 6*sizeof(XAKEPVERTEX), 0, D3DFVF_XYZ
| D3DFVF_DIFFUSE, D3DPOOL_DEFAULT, &lpVB ) ) )
return E_FAIL;
//Заполнение оного
VOID* pvtx;
if( FAILED( lpVB->Lock( 0, sizeof(vtx), (BYTE**)&pvtx, 0 ) ) )
return E_FAIL;
memcpy( pvtx, vtx, sizeof(vtx) );
lpVB->Unlock();
return S_OK;
}
Что ты, собственно, сейчас
и будешь делать ( я про рендеринг ). Все это
злобное деяние будет совершаться в функции
Render(). Естественно, сначала надо очистить
экранную поверхность ( на деле –
внеэкранную 😉 ) и з-буфер. Тут отличия от
седьмого дыхса нет, все так же выполняется
методом IDirect3DDevice8::Clear, даже флаги такие же
остались. Еще идентичным с предыдущей
версией является начало и завершение сцены.
Традиционные методы Begin- и EndScene имеют место
быть. Но вот кардинальные изменения
перетерпел сам рендеринг. Вот смотри:
сначала нужно определить источник потока
вершин методом
IDirect3DDevice8::SetStreamSource
(
UINT StreamNumber, //Номер создаваемого потока
IDirect3DVertexBuffer8* pStreamData, //Буфер вершин, в
котором оные находятся
UINT Stride //Размер одной вершины ( попросту –
структуры, которая описывает ее )
);
затем – определить
обработчик вершин ( это тот, который
программируемый ), если не хочешь геморроя (
я сам еще не пытался на нем писать, поэтому
не знаю, насколько серьезным может быть
этот геморрой ), то здесь можно поставить
всего лишь тип твоих вершин. Хы, а вот теперь
можешь спокойно рендерить методом DrawPrimitive,
у которого теперь всего 3 параметра. Первый
– тип примитива, второй – номер первой
обрабатываемой вершины, третий –
количество рисуемых примитивов. Вот всю
какашку ты нарисовал, а теперь ты должен
ведь показать свой труд. Воспользуйся
методом
IDirect3DDevice8::Present
(
CONST RECT* pSourceRect, //Прямоугольник источника, NULL
– вся поверхность
CONST RECT* pDestRect, //Прямоугольник назначения,
аналогично
HWND hDestWindowOverride, //Для оконных прог, - окошко, в
котором рисовать
CONST RGNDATA* pDirtyRegion //ГЫХЫЗЫХ, какая-то будущая
фишка
);
Вот так ты все и
зарендерил.
VOID Render()
{
//Очистка экрана и з-буфера
lpD3DD->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00, 1.0f, 0 );
//Начало рендеринга
lpD3DD->BeginScene();
//Рендеринг примитивов
lpD3DD->SetStreamSource( 0, lpVB, sizeof(XAKEPVERTEX) );
lpD3DD->SetVertexShader( D3DFVF_XYZ | D3DFVF_DIFFUSE );
lpD3DD->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 2 );
//Завершение рендеринга
lpD3DD->EndScene();
//Вывод на экран
lpD3DD->Present( NULL, NULL, NULL, NULL );
//Курсор - в зад
SetCursor( NULL );
}
Думаешь, этого с д3д
хватит? Ошибаешься кучерявый. Напоследок
надо дописать работу с матрицами и уборку
за собой. Матрица тебе нужна для того, чтобы
повернуть сцену на определенный угол. Пусть
все это будет происходить в функции UpdateMatrix().
Матрица, как ты уже видел в листинге
инициализации, хранится в структуре D3DXMATRIX.
Чтобы заполнить матрицу, так что она
поворачивала бы по какой-либо оси на
определенный угол, существует
вспомогательная функция библиотеки D3DX
D3DXMatrixRotation[X/Y/Z]. Я тебе посоветую повернуть
вокруг оси Y, - так нагляднее. Я даже не буду
объяснять эту функцию, так как у нее всего
два параметра – матрица и угол. Ну и теперь
это все надо трансформировать. Чтобы
листинг был не так мал, сразу здесь я
расскажу про уборку за собой всякого гадежа
в виде д3двских интерфейсов. У всех этих
интерфейсов есть метод Release(), которым ты и
воспользуешься при их деинициализации.
//Заполнение матриц
VOID UpdateMatrix()
{
//Заполнение мировой матрицы
D3DXMATRIX World;
D3DXMatrixRotationY( &World, timeGetTime()/350.0f );
lpD3DD->SetTransform( D3DTS_WORLD, &World );
}
//Функция завершения работы проги
VOID Release()
{
lpVB->Release();
lpD3DD->Release();
lpD3D->Release();
}
Вот на этом с д3д и кончим.
Дальше пойдут уже всякие банальные
виндошные обработки мессаг и создания
окошек. События по-старинке обрабатываются
в функции MsgProc(). Я помню, как-то задавал
задание на дом, чтобы заставить прогу
выходить при нажатии клавиши. Самое
странное, что с ним никто не справился ( мне,
почему-то, исходники никто не прислал :)) ).
Нажатие клавиши генерирует мессагу WM_CHAR (
при нажатии не на специальную батону ). Вот
при нем-то прога и выйдет.
//Обработка виндошных
мессаг
LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
switch( msg )
{
case WM_DESTROY: //Закрытие проги
PostQuitMessage( 0 );
return 0;
case WM_CHAR: //Нажатие клавиши
//Уборка территории
Release();
PostQuitMessage( 0 );
return 0;
}
return DefWindowProc( hWnd, msg, wParam, lParam );
}
Вот теперь функция WinMain, с
которой начинается выполнение проги. В
самом ее начале надо создать и
зарегистрировать виндошный класс, а затем
создать окно. Потом – инициализация д3д,
вершин и уборка курсора. А после – цикл
обработки мессаг, в котором как раз и будет
рендеринг. Вот, в общем-то и все.
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR,
INT )
{
//Создание виндошного класса
WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L,
GetModuleHandle(NULL), NULL, NULL, NULL, NULL, "Xakep3D", NULL };RegisterClassEx(
&wc ); //Его регистрация
//Создание окна
HWND hWnd = CreateWindow( "Xakep3D", "XAKEP3D",
WS_OVERLAPPEDWINDOW, 0, 0, 31337, 31337, GetDesktopWindow(), NULL, wc.hInstance,
NULL );
//Инициализация д3д
if( FAILED( Initialize( hWnd ) ) ) return -1;
//Создание вершин
if( FAILED( SetVertices() ) ) return -1;
//Курсор - в зад
SetCursor( NULL );
MSG msg;
//Цикл проги
while( msg.message!=WM_QUIT )
{
if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
else
{
//Заполнение всех матриц
UpdateMatrix();
Render();
}
}
return 0;
}
Вот теперь попробуй что-нибудь
убрать, или добавить. Не бойся
экспериментировать! А пока я пойду
скринсейвер на конкурс кодить ;). Может ты
тоже попробуешь написать, и твой окажется
круче ;). Ну, все. Счастливо!
ЗЫЖ Happy rendering 4u 🙂