DirectX 8: Создание и текстурирование простого трехмерного объекта.
Автор: Николай Александрович Сударкин
Ты уже умеешь инициализировать Direct3D, а значит можно на этой базе создавать трехмерные объекты. Чтобы было совсем интересно, натянем на наш трехмерный объект текстуру и заставим его вращаться!!! Кроме того, воспользуемся фильтрацией текстур и MipMapping'ом, чтобы получить более реалистичный результат почти без потери быстродействия. Попутно, научимся работать со вспомогательной библиотекой Direct3DX. Пример можешь скачать ЗДЕСЬ.
Итак, приступим. За "трехмерный объект" возьмем правильную четырехугольную пирамиду, а за текстуру - подредактированную фотографию каменной стены (первоначальный вариант текстуры взят с сайта www.3DCafe.com, на котором собрано огромное количество различных бесплатных для скачивания изображений).
Функции WinMain(), MessageProc() и InitD3D() будут очень похожи на аналогичные функции программы из предыдущей статьи, поэтому я буду объяснять, в основном, только модифицированные их части. Структура программы немного изменилась. Теперь более грамотно обрабатываются ошибки, которые могут возникнуть при выполнении той или иной части программы.
Рассмотренные функции исходника:
Функция WinMain()
Функция Initialization()
Функция InitD3D()
Функция InitTexture()
Функция InitScene
Функция DoMatrices()
Функция RenderScene()
Функция Deinitialization()
Функция MessageProc()
Функция WinMain()
Главная функция нашего приложения. Практически аналогична одноименной функции из предыдущей статьи. Единственное, что сильно изменилось - цикл обработки сообщений. Также, вынесены в отдельные функции инициализация и деинициализация приложения.
if (Initialization( hWnd)) { ShowWindow( hWnd, nCmdShow); 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 RenderScene( ); } } Deinitialization( );
Если инициализация проходит успешно, окно приложения отображается на экране и начинается цикл обработки сообщений, иначе деинициализация и выход из программы. Функция PeekMessage() проверяет очередь сообщений для текущей нити процесса и, если таковые имеются, помещает одно из них в структуру MSG. Флаг PM_REMOVE говорит о том, что сообщение из очереди удаляется, т.е. на нас возлагается ответственность его корректной обработки в любом случае. Это мы и делаем в следующих двух строчках. Если же очередь сообщений пуста, вызывается функция RenderScene(), которая, согласно ее названию, рендерит очередной кадр сцены. Как только поступает сообщение WM_QUIT, происходит деинициализация приложения (функция Deinitialization()).
Функция Initialization()
Осуществляет грамотную (т.е. с учетом ошибок) инициализацию D3D, текстур и сцены. Замечу, что все функции нашего приложения, в которых может возникнуть ошибка, имеют тип BOOL. Рассмотрим программный код:
CurrentFilter = D3DTEXF_NONE; if (!InitD3D( hWnd)) return FALSE; if ( !InitTexture( )) { MessageBox( NULL, "Не найден файл текстуры!", "Ошибка", MB_ICONERROR); return FALSE; } if ( !InitScene( )) return FALSE; return TRUE;
Что такое CurrentFilter? Забегая вперед, скажу, что наша программа будет использовать фильтрацию текстур, причем можно будет переключаться между различными фильтрами. Неплохо для первой программы с использованием D3D? ;-) Итак, первая строка кода говорит, что по умолчанию фильтром является D3DTEXF_NONE, т.е. фильтрация не используется. Затем вызываются три функции дополнительной инициализации, причем, если хотя бы в одной из них возникнет ошибка, функция Initialization() вернет значение FALSE, что в свою очередь остановит выполнение программы (вспомни функцию WinMain()) и приведет к деинициализации. Если функция InitTexture() возвращает значение FALSE, значит не удалось найти файл текстуры и перед аварийным завершением программы необходимо вывести предупредительное сообщение:
MessageBox(NULL, "Не найден файл текстуры!", "Ошибка", MB_ICONERROR);
Функция InitD3D()
Согласно названию, эта функция инициализирует D3D %) Перед созданием устройства рендеринга, необходимо настроить параметры D3DPRESENT_PARAMETERS. К параметрам, которые мы использовали в предыдущем приложении добавилось лишь две строки:
p_p.EnableAutoDepthStencil = TRUE;
p_p.AutoDepthStencilFormat = D3DFMT_D16;
Первая говорит о том, что при рендеринге будет использоваться Depth Buffer ("буфер глубины"), причем D3D будет управлять им автоматически. Когда разберешься с программой, попробуй поэкспериментировать: запусти программу в исходном виде, а затем проверь, что получится, если эти две строки удалить. Depth Buffer еще называют Z-Buffer'ом.
Рассмотрим принцип его использования на примере. Представь себе человека на фоне кирпичной стены... Теперь мысленно переведи эту "сцену" в Direct3D (для этого, необходимо задать человека и стену в виде набора текстурированных полигонов) и заставь его эту сцену отрендерить. То, что мы увидим на экране - всего лишь двумерная проекция нашей сцены. Т.о., D3D спроектировал на плоскость экрана стену и человека. Причем, мы увидели человека на фоне стены, а не стену на фоне человека, т.к. он стоит ближе к нам Ж-) Как же D3D "узнает" что именно проектировать на экран раньше, чтобы не было бессмысленных перекрытий? Для этого используется Z-Buffer! Z-Buffer представляет собой массив Z-координат каждой точки этой поверхности (будем называть ее числовой составляющей (ЧС) Z-Buffer'а). Ось Z направлена в плоскость экрана (от тебя). Каждой точке сопоставлен единственный элемент ЧС. Перед началом рендеринга Z-буфер может быть заполнен в каждой точке значением равным максимально возможным расстоянием от наблюдателя до объекта сцены. После этого, для каждого полигона в сцене выполняются следующие действия:
a. определяется область, которую будет занимать полигон при проектировании на плоскость экрана
b. для каждого пикселя этой области сравнивается Z-координата точки-прообраза, принадлежащей исходному полигону, с соответствующим значением ЧС
c. если соответствующее значение ЧС оказывается меньше, значит, рассматриваемая точка полигона не отображается, т.к. перекрыта другими объектами. Иначе, в ЧС для рассматриваемой точки записывается ее Z-координата, а в цветовую область заносится пиксель соответствующего цета.
Вот и весь принцип.
Далее в программе создается устройство рендеринга:
if(FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &p_p, &g_pD3DDevice))) { if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &p_p, &g_pD3DDevice))) return FALSE; }
Здесь программа пытается выбрать наиболее оптимальный режим для работы. Сначала она пробует создать аппаратное устройство рендеринга, т.е. когда все обсчеты графики ведет исключительно видеокарта (D3DDEVTYPE_HAL), но если это не удается (причин может быть много, но чаще всего она одна - старая видеокарта) D3D работает в режиме программной эмуляции, что прискорбно отражается на скорости и качестве графики. Едем дальше: Задается режим "выбраковки" тыльных сторон полигонов (подробней мы рассмотрим это дальше - в функции InitScene()):
g_pD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
Включаем равномерное освещение всей сцены:
g_pD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
Включаем поддержку Z-Buffer'а
g_pD3DDevice->SetRenderState(D3DRS_ZENABLE, TRUE);
Может возникнуть вопрос, для чего может понадобиться отключение Z-Buffer'а. Одно из применений - вывод на экран текста, который должен перекрывать все остальное.
Хотелось бы также подробней рассказать про функцию SetRenderState() интерфейса IDirect3DDevice8, которую мы уже встречали ранее. Эта функция вообще незаменима, т.к. с ее помощью можно задать огромное количество различных настроек, влияющих на процесс рендеринга. Вот описание этой функции:
HRESULT SetRenderState(D3DRENDERSTATETYPE State, DWORD Value );
State - тип изменяемого параметра
Value - новое его значение
Функция InitTexture()
Эта функции выполняет только одно действие - загружает текстуру из файла в память для дальнейшего использования. Загрузка текстуры вручную заняла бы у нас довольно обширный кусок программного кода - этому будет посвящена отдельная статья. Поэтому, используем функцию D3DXCreateTextureFromFile(), которую программисты Microsoft написали за нас :) Префикс "D3DX-" этой функции, говорит о том, что она взята из библиотеки D3DX. Эта вспомогательная библиотека включает в себя очень много полезных функций, как для математических операций (в основном, работы с матрицами), так и для загрузки изображений, формирования стандартных геометрических объектов (сфера, куб и т.п.) и многого другого. Тем не менее, эти функции написаны для общих задач. Когда ты будешь писать конкретную программу, требующую быстродействия, советую не использовать D3DX, а писать аналоги его функций самому.
Вот описание функции D3DXCreateTextureFromFile():
HRESULT D3DXCreateTextureFromFile(LPDIRECT3DDEVICE8 pDevice, LPCSTR pSrcFile, LPDIRECT3DTEXTURE8* ppTexture );
pDevice - указатель на устройство рендеринга
pSrcFile - текстовая строка, содержащая путь к файлу-текстуре
ppTexture - адрес переменной, которая будет содержать указатель на текстуру
Функция InitScene()
Здесь происходит инициализация единственного объекта в сцене - четырехугольной пирамиды.
Как известно, в основании правильной четырехугольной пирамиды (SABCD) лежит квадрат (ABCD). Если сторона квадрата a, то высота пирамиды h=a/sqrt(2). Теперь, зная a, мы можем задать в трехмерном пространстве координаты всех пяти вершин пирамиды. В DirectX используется левосторонняя (left-handed) система координат (СК). Направление оси Z в левосторонней и правосторонней СК можно определить, пользуясь правилом соответственно левой и правой руки. Вот, как с помощью этого правила найти куда направлена ось Z в левосторонней СК: вытяни левую руку ладонью вверх и собери четыре пальца (все, кроме большого) вместе в плоскости ладони. Большой палец расположи перпендикулярно остальным четырем (тоже в плоскости ладони). Отлично! (Ты смог это! - прим. редактора) Теперь, если направить ось X вдоль четырех пальцев, а ось Y - вверх, то большой палец укажет направление оси Z.
Координаты вершин пирамиды в пространстве можно записать следующим образом (не обращай пока внимание на последние 3 параметра каждой вершины):
float a=6.0; #define vertA {-a/2, a/2, 0.0f, 0xffffffff, 0.0f, 1.0f,} #define vertB {-a/2, -a/2, 0.0f, 0xffffffff, 0.0f, 0.0f,} #define vertC {a/2, -a/2, 0.0f, 0xffffffff, 1.0f, 0.0f,} #define vertD {a/2, a/2, 0.0f, 0xffffffff, 1.0f, 1.0f,} #define vertS {0.0f, 0.0f, (float)( a/sqrt( 2)), 0xffffffff, 0.5f, 0.5f,}
В D3D трехмерную модель можно задать различными способами. В нашем случае будем использовать TRIANGLELIST для которого фигура задается последовательностью треугольников. Когда задается треугольник, Direct3D сам определяет его лицевую и тыльную стороны по порядку следования вершин в массиве. При рендеринге, D3D автоматически "выбраковывает" тыльные стороны треугольников. Это заметно повышает скорость работы приложения. Но необходимо указать D3D в каком именно порядке задаются вершины лицевой стороны треугольников. Это делается с помощью той самой волшебной функции SetRenderState().
Итак, чтобы описать culling (выбраковку) тыльных сторон треугольников, вершины которых расположены в массиве по часовой стрелке (clockwise), необходимо написать следующее:
g_pD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
Для хранения вершин в D3D используются Vertex Buffer'ы (в дальнейшем, VB. Не путать с Visual Basic'ом :)). В зависимости от конкретной программы, VB'ы могут быть разных форматов. Например, если требуется написать программу, которая рисует на экране набор одноцветных точек, то для задания любой из точек требуется три числа, содержащих ее координаты в пространстве. Если точки должны отличаться по цвету, вводим четвертый параметр - цвет точки. Вроде бы все просто... Единственная сложность - мы как-то должны "сообщить" D3D в каком именно формате хранятся вершины в массиве, чтобы в процессе рендеринга не возникло путаницы. Впервые это нужно сделать в момент создания VB, затем перед рендерингом. Формат задается в виде комбинации флагов D3DFVF_*, полный список которых приведен в документации к D3D8. Нам же понадобятся лишь 3 флага:
#define D3DFVF_MYVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE|D3DFVF_TEX1)
D3DFVF_XYZ - вершина задается тремя координатами в пространстве (а может задаваться и четырьмя - при D3DFVF_XYZRHW)
D3DFVF_DIFFUSE - вершина содержит цвет, который влияет на рассеяние света
D3DFVF_TEX1 - вершина содержит две текстурные координаты
Т.к. запись
D3DFVF_XYZ|D3DFVF_DIFFUSE|D3DFVF_TEX1
интерпретируется компилятором в точности, как и
D3DFVF_XYZ|D3DFVF_TEX1|D3DFVF_DIFFUSE
, значит порядок расположения данных в памяти в этом месте программы не задается. На программиста накладываются обязательства следовать схеме расположения данных, приведенной в руководстве D3D8 (раздел "About Vertex Formats").
Ну, надеюсь с этим все ясно. Теперь нужно занести вершины всех полигонов пирамиды в память. Для этого создаем массив из вершин и заполняем его данными:
MYVERTEX Vertices[] = { vertS, vertA, vertD, vertS, vertB, vertA, vertS, vertC, vertB, vertS, vertD, vertC, };
Следующий шаг - нужно создать буфер вершин (VB) требуемого размера и формата. Пирамида будет отображена на экране так, что ее нижнего основания не будет видно, значит можно обойтись лишь 4-мя полигонами вместо 6-ти. Здесь я следовал правилу, которое прочитал в руководстве DX: "Remember, the fastest polygons are the ones you don't draw" (что в переводе означает: "Помни, наиболее быстрые полигоны - это те, которые ты не рисуешь"). Создание VB производится функцией CreateVertexBuffer:
HRESULT CreateVertexBuffer(UINT Length, DWORD Usage, DWORD FVF, D3DPOOL Pool, IDirect3DVertexBuffer8** ppVertexBuffer );
Length - длина VB в байтах
Usage - дополнительная информация о VB, которую D3D использует для создания оптимального VB
FVF - формат вершин, которые будут храниться в VB
Pool - в какой памяти создавать VB (можно создать его как в видеопамяти, так и в RAM)
ppVertexBuffer - адрес переменной, которая будет содержать указатель на созданный VB
Всего для хранения полигонов пирамиды используется 4*3*sizeof(MYVERTEX) байт (4 полигона, по 3 вершины в каждом).
if(FAILED( g_pD3DDevice->CreateVertexBuffer( 4*3*sizeof( MYVERTEX), 0, D3DFVF_MYVERTEX, D3DPOOL_DEFAULT, &g_pVB))) { return FALSE; }
Остается заполнить буфер вершинами. Для операций заполнения в DX (не только в D3D) используется пара команд Lock() и Unlock(). Команда Lock() возвращает адрес памяти, по которому расположен первый байт буфера. При этом вся память, отведенная под буфер как бы "запирается", и становится недоступной для других приложений. Операция "отпирания" памяти производится командой Unlock(). После запирания памяти, скопируем данные с помощью Си'шной функции memcpy().
VOID* pVertices; if(FAILED( g_pVB->Lock( 0, sizeof( Vertices), ( BYTE**)&pVertices, 0))) return FALSE; memcpy( pVertices, Vertices, sizeof( Vertices)); g_pVB->Unlock( );
Функция DoMatrices()
Я считаю, что это - самая сложная для понимания функция. Разговор о матрицах выходит за формат данной статьи, т.к. это очень обширная тема. Советую почитать статьи JM'а по этому поводу (скажу по секрету - он фанат матриц ;o)). Но вкратце, я все равно расскажу о матрицах :-)
У нас есть трехмерное пространство сцены, которое содержит вершины всех объектов, есть камера - глаз, с помощью которого мы видим это пространство, а также плоскость экрана монитора, на которую осуществляется проектирование. Все это ("мир", камера, операция проектирования) может быть выражено тремя матрицами: World Matrix (мировая матрица), View Matrix (видовая матрица) и Projection Matrix (проекционная матрица).
Вычислять эти матрицы "вручную" довольно сложно, поэтому воспользуемся функциями D3DX. Для матриц создан специальный тип данных D3DMATRIX. В библиотеке D3DX он расширен до типа данных D3DXMATRIX, в который добавлены арифметические операции с матрицами, и некоторые другие удобные свойства.
Функция D3DXMatrixIdentity() строит единичную матрицу.
Функция D3DXMatrixRotationZ() строит матрицу вращения относительно оси Z на заданный угол.
Функция D3DXMatrixLookAtLH() строит видовую матрицу. Параметры этой функции задают точку, в которую будет смотреть камера. Постфикс -(LH) говорит о том, что матрица будет действительна для левосторонней системы координат (аналогично -(RH) для правосторонней)
Функция D3DXMatrixPerspectiveFovLH() строит проекционную матрицу.
Для того, чтобы "заставить" устройство рендеринга использовать только что созданные нами матрицы, существует функция SetTransform():
HRESULT SetTransform(D3DTRANSFORMSTATETYPE State, CONST D3DMATRIX* pMatrix );
State - тип матрицы, которую нужно изменить (мировая, видовая, проекционная и т.д.)
pMatrix - указатель на "матрицу-заменитель" :)
Вот что нам требуется от каждой из матриц:
a. Мировая
Сделаем так, чтобы пирамида с течением времени равномерно вращалась вокруг оси Z:
D3DXMATRIX matWorld; D3DXMatrixIdentity(&matWorld); D3DXMatrixRotationZ( &matWorld, GetTickCount( )/1024.0f); g_pD3DDevice->SetTransform( D3DTS_WORLD, &matWorld);
b.Видовая
Камера должна смотреть на пирамиду сбоку, причем не должно быть видно нижнего основания пирамиды (помнишь, мы выбросили два полигона основания?):
D3DXMATRIX matView; D3DXMatrixLookAtLH(&matView, &D3DXVECTOR3( 5.0f, 5.0f, 6.5f), &D3DXVECTOR3( 0.0f, 0.0f, 1.0f), &D3DXVECTOR3( 0.0f, 0.0f, 1.0f)); g_pD3DDevice->SetTransform( D3DTS_VIEW, &matView);
c. Проекционная
D3DXMATRIX matProj; D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/3, 1.0f, 1.0f, 100.0f); g_pD3DDevice->SetTransform( D3DTS_PROJECTION, &matProj );
Здесь D3DX_PI/3 - это поле зрения (field of view) камеры. Попробуй поэкспериментировать с этим параметром.
Функция RenderScene()
Собственно, здесь и происходит рендеринг сцены. Как всегда, он начинается с очистки окна и Z-Buffer'а:
g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB( 100, 100, 100), 1.0f, 0);
Затем, подготавливаем D3D к началу сцены.
g_pD3DDevice->BeginScene();
Пересчитываем матрицы:
DoMatrices();
Теперь настроим текстуру. И вообще, здесь подходящее место для того, чтобы вкратце рассказать о том, что такое текстура и текстурные координаты! Итак... Текстура - это графическая картинка, которая используется для натягивания на трехмерный (и не только) объект, что придает ему реалистичный вид (правда, это зависит от текстуры и от того, кто и на что ее натягивает :)) ). При текстурировании объекта, каждая его вершина должна иметь текстурные координаты, т.е. числа от 0 до 1, задающие привязку к конкретному месту текстуры. Рассмотрим пример, т.е. нашу четырехугольную пирамиду. Она была выбрана не случайно именно четырехугольной! Представь себе квадратный (для простоты), очень эластичный лист резины, на котором изображена каменная стена (текстура). Теперь "схвати" его за центр и тяни вверх (при этом края квадрата должны оставаться на месте). Т.к. лист эластичный, он легко поддастся и начнет растягиваться. Вместе с ним будет растягиваться и изображение стены. Тяни до тех пор, пока лист не превратится в правильную четырехугольную пирамиду без нижнего основания. Заморозь полученный объект, чтобы он не вернулся в первоначальное положение. Все! Еще раз вернемся к месту, где задаются вершины пирамиды:
float a=6.0; #define vertA {-a/2, a/2, 0.0f, 0xffffffff, 0.0f, 1.0f,} #define vertB {-a/2, -a/2, 0.0f, 0xffffffff, 0.0f, 0.0f,} #define vertC {a/2, -a/2, 0.0f, 0xffffffff, 1.0f, 0.0f,} #define vertD {a/2, a/2, 0.0f, 0xffffffff, 1.0f, 1.0f,} #define vertS {0.0f, 0.0f, (float)( a/sqrt( 2)), 0xffffffff, 0.5f, 0.5f,}
0xffffffff (белый цвет) - цветовой "вес" вершины. Белый цвет означает, что текстура в данной вершине будет того же цвета и яркости, что и в оригинале. Если заменить цвет на 0x00000000, то вершина будет черной и при рендеринге вокруг нее образуется черное пятно. Попробуй! =)
Последние два числа являются текстурными координатами. Вернемся к мысленному эксперименту с квадратным листом... (до того, как мы его деформировали) Возьмем его левый верхний угол за начало координат. Ось X направим вправо, ось Y - вниз. Тогда правый нижний угол будет иметь координаты (1,1). Как ты уже догадался, центр квадрата (проекция вершины S пирамиды на плоскость основания) имеет координаты (0.5,0.5). Вот так задаются текстурные координаты.
Вернемся к программе. У нас будет одноуровневая текстура, первым и единственным уровнем которой будет загруженная ранее g_pTexture:
g_pD3DDevice->SetTexture(0, g_pTexture);
При рендеринге на текстуры могут накладываться фильтры, что сглаживает многие недостатки. Изюминкой программы является то, что тип фильтра можно менять прямо во время исполнения (клавишами F1, F2, F3, F4). Кроме того, как я обещал в начале статьи, используем MipMapping! Но для начала расскажу, что он собой представляет...
MipMap - это цепочка текстур, каждая последующая из которых является менее детализированным вариантом предыдущей. Уменьшение детализации на один уровень достигается путем сокращения длины и ширины текстуры в два раза. Цепочка генерируется до тех пор, пока размер одной из сторон текстуры не становится равным 1. Допустим, что текстурированный объект удаляется от наблюдателя. Сначала на него накладывается текстура с максимальным разрешением, затем, по мере удаления, текстура переключается на свой менее детализированный вариант. Согласись, на далекий объект, который занимает на экране всего один пиксель, глупо натягивать текстуру размером 100 Kb. Таким образом, благодаря некоторым дополнительным затратам памяти на MipMap-текстуры, заметно увеличивается быстродействие рендеринга и, вообще говоря, его качество. Во время переключения между детализациями, визуально может быть заметен скачок. Его можно сгладить, используя фильтрацию, что мы и сделаем (напомню, что текущий тип фильтра хранится в переменной CurrentFilter):
g_pD3DDevice->SetTextureStageState(0, D3DTSS_MAGFILTER, CurrentFilter); g_pD3DDevice->SetTextureStageState( 0, D3DTSS_MINFILTER, CurrentFilter); g_pD3DDevice->SetTextureStageState( 0, D3DTSS_MIPFILTER, CurrentFilter);
Перед рендерингом из VB, необходимо задать сам буфер и формат вершин, что делается следующими двумя строками:
g_pD3DDevice->SetStreamSource(0, g_pVB, sizeof( MYVERTEX)); g_pD3DDevice->SetVertexShader( D3DFVF_MYVERTEX);
И, наконец! Все готово для рендеринга! Делается это всего одной функцией DrawPrimitive(). Первый параметр говорит о том, в каком виде хранятся в VB вершины. В нашем случае, объект задается последовательностью треугольников (D3DPT_TRIANGLELIST). Второй параметр говорит о том, с какой по номеру вершины из VB начинать отрисовку. Третий параметр - количество примитивов (в нашем случае треугольников), которые требуется отрендерить.
g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 4);
Завершаем сцену:
g_pD3DDevice->EndScene();
Отображаем бэк-буфер на экране:
g_pD3DDevice->Present(NULL, NULL, NULL, NULL);
Функция Deinitialization()
Для освобождения большинства (если не всех) видов ресурсов в D3D используется функция Release():
if(g_pTexture != NULL) g_pTexture->Release( ); if( g_pVB != NULL) g_pVB->Release( ); if( g_pD3DDevice != NULL) g_pD3DDevice->Release( ); if( g_pD3D != NULL) g_pD3D->Release( );
Также, перед завершением работы нужно освободить ранее зарегистрированный класс окна:
UnregisterClass("PyramidClass", wclass.hInstance);
Функция MessageProc()
Программа будет обрабатывать только два типа сообщений: WM_KEYDOWN (нажата клавиша) и WM_DESTROY (уничтожено окно приложения).
При поступлении сообщения WM_KEYDOWN получаем виртуальный код нажатой клавиши:
case WM_KEYDOWN: int VK; VK = (int)wParam;
Из клавиш, обрабатываем только Esc, F1, F2, F3, F4. При нажатии на Esc программа должна завершиться, как ни парадоксально это звучит :)) Если нажата клавиша F1-F4, должен смениться тип фильтра (переменная CurrentFilter) и заголовок окна:
switch(VK) { case VK_ESCAPE: PostQuitMessage( 0); return 0; //... case VK_F2: CurrentFilter = D3DTEXF_POINT; SetWindowText( hWnd, "D3D Pyramid (Filter=D3DTEXF_POINT)"); break; //... }
При поступлении сообщения WM_DESTROY, выполняются действия, аналогичные производящимся при обработке клавиши Esc:
case WM_DESTROY: PostQuitMessage(0); return 0;
Приехали! В принципе, вышеизложенного материала достаточно для самостоятельного написания простых 3D-приложений (для написания игры необходимо, как минимум, уметь работать с матрицами). Рекомендую поэкспериментировать с примером к данной статье. Попробуй поиграть настройками D3D, параметрами функций. Дай волю своему воображению! И, конечно же, попытайся написать что-нибудь сам (например, вращающийся треугольник или куб). Только так можно научиться. Накапливай опыт...
Удачи!
Примечания:
1. Помни, что для компиляции программ, использующих D3DX, необходимо подключить библиотечный файл d3dx8.lib (вместе с d3d8.lib)! Еще раз напомню, как это можно сделать:
a. Project->Settings...
b. Вкладка "Link"
c. В строке "Object/library modules" добавь в начало "d3d8.lib d3dx8.lib"
d. Должно получиться примерно следующее: "d3d8.lib d3dx8.lib kernel32.lib user32.lib gdi32.lib..."
2. Программе необходим файл texture.jpg - он должен лежать в той же директории, что и exe'шник.
27 мая 2002