Дарова,
программер! Типа, давно не виделись ;). Ну, э-э…
У меня на то причины были :)). О чем я в
прошлый раз рассказывал? А! Точно, о дх
Графигз 8. Сегодня, продолжая эту старую
тему, я расскажу о том, как накладывать
текстуры.
Шо цэ такэ
Для тех, кто в
бронетранспортере ( в танке, в коляске, в
международной космической станции ):
текстурой называется рисунок, который
натягивается на примитив ( чаще всего
полигон ). Существует такое понятие, как
координаты текстуры. Они присваиваются
каждой вершине затекстуренного примитива и
обозначают, какой элемент текстуры будет в
этом месте. Координаты 0,0 – верхний левый
угол текстуры; 1,1 – правый нижний. Таким
образом можно растягивать( например, 0.3,0.7 )/сжимать
текстуру ( 8,8 ). В последнем случае по
умолчанию текстура повторяется на полигоне.
Численное значение ширины, так же как и
высоты, текстуры должно быть степенью
двойки. Так же есть понятие “тексель”. Это
– пиксель на изображении текстуры.
Естественно, что может возникнуть случай,
когда на экране несколько пикселей могут
показывать лишь один тексель, или в одном
пикселе – несколько текселей. Существуют
два решения такой проблемы. Один –
мипмэппинг. Это когда вместо одной текстуры
используются несколько так называемых
мипмэп-уровней, ширина и высота каждого
следующего из которых равна ширине и высоте
предыдущего, деленного на два, создаваемых
при загрузке текстуры. При уменьшении
размера мипмэп-уровня уменьшается его
качество. Второе – уменьшение текстуры в
реальном времени. Надо сказать, что
мипмэппинг используется для увеличения
скорости рендеринга при больших затратах
памяти, а mag/min ( от анг. magnificate – увеличение,
minificate – уменьшение ) – при недостатках
памяти и при больших размерах текстур ( ну
не станешь же ты сохранять, допустим, по 10
мип-уровней четырех ( а их часто значительно
больше ) текстур формата 2048х2048х24, - тут и 2-х
гигов оперативки не хватит 8)! Я, конечно,
понимаю, что ее у тебя 32 гига, но пойми, ( если
у тебя, конечно, не Voodoo31337 62674Gb ) шина АГП – не
резиновая, и уже при рендеринге ОДНОЙ
текстуры, занимающей 23 Мб, fps падает до 20 ).
Существуют различные типы mag/min фильтра.
Основные – LINEAR и NEAREST. Первый – плавное
изменение цвета от текселя к текселю.
Второй – берется усредненное значение
соседних текселей.
Строим из себя
художника
Самыми первыми
твоими действиями будет создание двух
текстур. Почему двух?? Я тебе просто покажу,
как осуществить мультитекстуринг, т.е.
наложить сразу несколько текстур. После
нескольких минут работы в Линуксоидном The
GIMP’е ( Gimp – rulezzzzz!! Не в пример фотожопу ) у
меня вышло следующее:
Кирпич нужен для
того, чтобы показать тебе повторение
текстуры. Логу хакера сохрани как xakep.bmp, а
кирпич… ну пусть kirpich.bmp.
А ты не забыл
открыть Xakep3D?
Ну что, дружок,
щас кодить будешь ( а ты думал, я сюда пришел
тебя учить, как валенки вязать 😉 ). Ты же
читал предыдущую мою статью? Ай да молодец!
Ну вот и открывай Xakep3D. Итак, надо добавить
несколько переменных, коими являются свет (
D3DLIGHT8 ), материал ( D3DMATERIAL8 ) и две текстуры (
LPDIRECT3DTEXTURE8 ). Далее надо убрать из структуры
XAKEPVERTEX описание цвета и добавить описание
вектора нормали ( перпендикуляра к
поверхности ) и двух координат текстур,
ОБЯЗАТЕЛЬНО в порядке как это сделал я. И
еще надо создать ей новое описание.
Листинг №1, main.cpp
– переменные, структура XAKEPVERTEX, ее описание
LPDIRECT3D8 lpD3D; //Интерфейс
д3д
LPDIRECT3DDEVICE8 lpD3DD; //Интерфейс д3д девайса
LPDIRECT3DVERTEXBUFFER8 lpVB; //Интерфейс буфера вершин
D3DLIGHT8 d3dLight; //Освещение
D3DMATERIAL8 mat; //Материал
LPDIRECT3DTEXTURE8 lpTexLin; //Текстура
LPDIRECT3DTEXTURE8 lpTexRep; //Еще одна текстура
struct XAKEPVERTEX //Объявление
структуры, которая хранит параметры
вершины
{
float x, y, z; // Координаты
float nx, ny, nz; // Вектор нормали
float tu, tv; // Координаты текстуры 1
float tu1, tv1; // Координаты текстуры 2
};
#define D3DFVF_XAKEPVERTEX ( D3DFVF_XYZ
| D3DFVF_NORMAL | D3DFVF_TEX2 ) //Определение
описания типа XAKEPVERTEX
В функции Initialize
что-то менять надо только после создания д3д
девайса. Во-первых, разреши освещение, т.е.
D3DRS_LIGHTING, TRUE. Во-вторых сразу после установки
параметров рендеринга инициализируй свет,
обнули его сначала, а потом – поставь его
цвет ( Diffuse и Ambient ) в белый. Примени и разреши
его ( методы SetLight и LightEnable интерфейса
IDirect3DDevice8; первые параметры у обоих – номер
лампы, а второй – у Set~ - Адрес лампы, у ~Enable –
TRUE/FALSE ). То же проделай с материалом, только
альфу можешь не ставить, у тебя все равно не
разрешен альфа-блендинг ( люблю оптимизацию,
мать ее за ногу 😉 ). Применять его так же, как
и в дх7. Дальше идет установка матрицы вида.
Тут только поменять координаты камеры в (
10.0f, 10.0f, -10.0f ). В установках матрицы проекции
поменяй дальность на 20.0f. Ну и, наконец,
загрузи, текстуры. Дабы не геморроиться
сделай это при помощи функции
D3DXCreateTextureFromFile. Первый параметр – д3д девайс,
второй – имя файла, третий – адрес
указателя интерфейса текстуры ( именно
АДРЕС УКАЗАТЕЛЯ ). Теперь поставь параметр
D3DTSS_COLOROP текстуры слоя 0 в значение D3DTOP_ADD.
Делается это методом IDirect3DDevice8::SetTextureStageState.
Параметры: слой текстуры, параметр,
значение. Пресловутый D3DTSS_COLOROP отвечает за
то, каким образом будет получаться цвет
пикселя на затекстурированном примитиве.
Тут можно оперировать только двумя
операндами ( параметры D3DTSS_COLORARG1 и D3DTSS_COLORARG2
). Их можно складывать, умножать, вычитать и
водить гулять :). Первый операнд-аргумент по-умолчанию
– цвет соответствующего текселя, второй –
цвет вершины после обсчета вычисления ( это
в 0-м слое текстур, в последующих же –
соответствующий цвет предыдущего слоя ).
COLOROP по-умолчанию стоит как D3DTOP_MODULATE, т.е.
аргументы перемножаются, а ты, хитрый и
нехороший, переставишь его в D3DTOP_ADD,
суммировать ты их будешь то есть. Таким
образом ты добьешься эффекта, очень
похожего на освещение по-Фонгу. Далее, для
первого и второго слоя текстур фильтр
увеличения D3DTSS_MAGFILTER поменяй на D3DTEXF_LINEAR. Ну
ты понял, о чем я.
Листинг №2, - main.cpp,
функция Initialize.
HRESULT Initialize( HWND hWnd )
{
…………………Создание д3д девайса………………………
//Заднюю сторону у полигона не обрезать
lpD3DD->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
//Гы, а здесь
освещение будет
lpD3DD->SetRenderState( D3DRS_LIGHTING, TRUE );
ZeroMemory( &d3dLight, sizeof(
D3DLIGHT8 ) ); //Инициализация света
d3dLight.Diffuse.r = 1; //Цвет
d3dLight.Diffuse.g = 1;
d3dLight.Diffuse.b = 1;
d3dLight.Ambient.r = 1;
d3dLight.Ambient.g = 1;
d3dLight.Ambient.b = 1;
d3dLight.Position.x = 2; //Координаты
d3dLight.Position.y = 2;
d3dLight.Position.z = -2;
d3dLight.Attenuation2 = 1; //Параметр угасания
d3dLight.Range = 10; //Радиус действия
lpD3DD->SetLight( 0, &d3dLight ); //Применение
света
lpD3DD->LightEnable( 0, TRUE ); //Разрешение
ZeroMemory( &mat, sizeof( D3DMATERIAL8 ) ); //Инициализация
материала
mat.Diffuse.r = 1; //Цвет
mat.Diffuse.g = 1;
mat.Diffuse.b = 1;
mat.Ambient.r = 1;
mat.Ambient.g = 1;
mat.Ambient.b = 1;
lpD3DD->SetMaterial( &mat ); //Применение материала
//Положение камеры
D3DXMATRIX View;
D3DXMatrixLookAtLH( &View, &D3DXVECTOR3( 10.0f, 10.0f, -10.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, 20.0f );
lpD3DD->SetTransform( D3DTS_PROJECTION, &Proj );
D3DXCreateTextureFromFile( lpD3DD,
"xakep.bmp", &lpTexLin ); //Загрузка текстур
D3DXCreateTextureFromFile( lpD3DD, "kirpich.bmp", &lpTexRep );
lpD3DD->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_ADD );
//Каким образом будет получаться цвет
lpD3DD->SetTextureStageState( 0,
D3DTSS_MAGFILTER, D3DTEXF_LINEAR );
//Линейный фильтр увеличения
lpD3DD->SetTextureStageState( 1, D3DTSS_MAGFILTER, D3DTEXF_LINEAR );
//Аналогично для второй
текстуры
return S_OK;
}
А вот функция
инициализации вершин значительно
увеличится в размере. Теперь у тебя будет не
два треугольника, а три четырехугольника.
Вернее, поверхности. Первую можно
представить как ( 0, Y, -Z ), вторую - ( X, Y, 0 ),
третью – ( X, 0, -Z ). Глянь на рисунок, сам
поймешь:
Ну, я думаю, все
понятно. “ Так почему она разрастется, ведь
тут нужно всего лишь 6 полигонов??” –
спросишь ты. Ну так сделай с шестью
полигонами – увидишь. Ну что, идти гадить не
тянет? Все дело в том, что тогда будет
глючить освещение. Не, ну работать
теоретически оно нормально будет, но вот
даже с моделью Гуро будет явно видно, что
поверхность составлена из 2-х треугольников,
и таких рулезных бликов, которые ты здесь
наблюдаешь, ты не заметишь ( честно
признаюсь, даже здесь детализация
недостаточна, если присмотришься – увидишь
). Поэтому придется составлять элементарную
плоскость из 542(!) полигонов. Я ведь уже
говорил, что люблю оптимизацию? Ну так вот,
все это 542 полигона я умещу в 544 вершины. И
так для каждой поверхности. Итого тебе
понадобится 1632 вершины. Заменяй старую
инициализацию XAKEPVERTEX vtx[] = { блаблаблабла };
на XAKEPVERTEX vtx[1632];. Алгоритмов разбиения
плоскости на полигоны существует туева
хуча, о том, как это сделал я, рассказывать
не буду ( долго это ). Если понадобится, сам
разберешься ( заодно и потренируешься
разбирать чужие алгоритмы, для кракинга
пригодится 😉 ). Естественно, потребуется
поменять размер буфера вершин и размер при
копировании, описание структуры XAKEPVERTEX.
Дабы разгрузить функцию Render, после всего,
описанного выше, надо применить этот буфер
и обработчик вершин.
Листинг №3, - main.cpp,
функция SetVertices.
HRESULT SetVertices()
{
//Параметры вершин полигонов
XAKEPVERTEX vtx[1632];
int g = 0;
for( float z = -4; z < 0; z += 0.25 ) //Получение координат
вершин полигонов плоскостей ( нехило, да 😉 )
{
for( float y = 0; y <= 4; y += 0.25 )
{
vtx[g].x = 0; vtx[g].y = y; vtx[g].z = z;
vtx[g].nx = 1; vtx[g].ny = y; vtx[g].nz = z;
vtx[g].tu = z/4; vtx[g].tv = y/4;
vtx[g+544].x = -z; vtx[g+544].y = y; vtx[g+544].z = 0;
vtx[g+544].nx = -z; vtx[g+544].ny = y; vtx[g+544].nz = -1;
vtx[g+544].tu = -z; vtx[g+544].tv = y;
vtx[g+1088].x = y; vtx[g+1088].y = 0; vtx[g+1088].z = z;
vtx[g+1088].nx = y; vtx[g+1088].ny = 1; vtx[g+1088].nz = z;
vtx[g+1088].tu1= z/4; vtx[g+1088].tv1 = y/4;
vtx[g+1088].tu = z; vtx[g+1088].tv = y; g++;
vtx[g].x = 0; vtx[g].y = y; vtx[g].z = z+0.25;
vtx[g].nx = 1; vtx[g].ny = y; vtx[g].nz = z+0.25;
vtx[g].tu = (z+0.25)/4; vtx[g].tv = y/4;
vtx[g+544].x = -z-0.25; vtx[g+544].y = y; vtx[g+544].z = 0;
vtx[g+544].nx = -z-0.25; vtx[g+544].ny = y; vtx[g+544].nz = -1;
vtx[g+544].tu = (-z-0.25); vtx[g+544].tv = y;
vtx[g+1088].x = y; vtx[g+1088].y = 0; vtx[g+1088].z = z+0.25;
vtx[g+1088].nx = y; vtx[g+1088].ny = 1; vtx[g+1088].nz = z+0.25;
vtx[g+1088].tu1 = (z+0.25)/4; vtx[g+1088].tv1 = y/4;
vtx[g+1088].tu = z+0.25; vtx[g+1088].tv = y; g++;
}
z += 0.25;
for( y = 4; y >= 0; y -= 0.25 )
{
vtx[g].x = 0; vtx[g].y = y; vtx[g].z = z;
vtx[g].nx = 1; vtx[g].ny = y; vtx[g].nz = z;
vtx[g].tu = z/4; vtx[g].tv = y/4;
vtx[g+544].x = -z; vtx[g+544].y = y; vtx[g+544].z = 0;
vtx[g+544].nx = -z; vtx[g+544].ny = y; vtx[g+544].nz = -1;
vtx[g+544].tu = -z; vtx[g+544].tv = y;
vtx[g+1088].x = y; vtx[g+1088].y = 0; vtx[g+1088].z = z;
vtx[g+1088].nx = y; vtx[g+1088].ny = 1; vtx[g+1088].nz = z;
vtx[g+1088].tu1= z/4; vtx[g+1088].tv1 = y/4;
vtx[g+1088].tu = z; vtx[g+1088].tv = y; g++;
vtx[g].x = 0; vtx[g].y = y; vtx[g].z = z+0.25;
vtx[g].nx = 1; vtx[g].ny = y; vtx[g].nz = z+0.25;
vtx[g].tu = (z+0.25)/4; vtx[g].tv = y/4;
vtx[g+544].x = -z-0.25; vtx[g+544].y = y; vtx[g+544].z = 0;
vtx[g+544].nx = -z-0.25; vtx[g+544].ny = y; vtx[g+544].nz = -1;
vtx[g+544].tu = (-z-0.25); vtx[g+544].tv = y;
vtx[g+1088].x = y; vtx[g+1088].y = 0; vtx[g+1088].z = z+0.25;
vtx[g+1088].nx = y; vtx[g+1088].ny = 1; vtx[g+1088].nz = z+0.25;
vtx[g+1088].tu1 = (z+0.25)/4; vtx[g+1088].tv1 = y/4;
vtx[g+1088].tu = z+0.25; vtx[g+1088].tv = y; g++;
}
}
//Создание буфера
вершин
if( FAILED( lpD3DD->CreateVertexBuffer(
1632*sizeof(XAKEPVERTEX), 0,
D3DFVF_XAKEPVERTEX, D3DPOOL_DEFAULT, &lpVB ) ) )
return E_FAIL;
//Заполнение
оного
VOID* pvtx;
if( FAILED( lpVB->Lock( 0, 0, (BYTE**)&pvtx, 0 ) ) )
return E_FAIL;
memcpy( pvtx, vtx, sizeof(XAKEPVERTEX)*1632 );
lpVB->Unlock();
lpD3DD->SetStreamSource( 0, lpVB,
sizeof(XAKEPVERTEX) ); //Применение этого буфера
вершин
lpD3DD->SetVertexShader( D3DFVF_XAKEPVERTEX );
return S_OK;
}
В функции Render
удали применение буфера вершин и
определение типа. Непосредственно перед
рендерингом ( DrawPrimitive ) вызови метод
IDirect3DDevice8::SetTexture. У него два параметра: номер
слоя текстуры ( 0 ) и адрес интерфейса
текстуры ( lpTexLin ). Параметры вызова метода
DrawPrimitive надо изменить. Тип примитива –
D3DPT_TRIANGLESTRIP, начальная вершина – 0, число
примитивов – 542. Это ты нарисовал первую
поверхность. Теперь надо поменять текстуру
0-го слоя на кирпичи ( т.е. lpTexRep ) и снова
вызвать метод DrawPrimitive, но номер начальной
вершины будет 544. И, наконец, третья
поверхность. На ней будет мультитекстура.
Так как на 0-м слое текстур у тебя уже есть
кирпичи, то на 1-й слой надо поставить логу
Хакера. Делаешь это все тем же методом SetTexture.
Параметр COLOROP 1-го слоя текстур установи в
D3DTOP_ADD, т.к. он по-умолчанию стоит в D3DTOP_DISABLE, т.е.
1-й слой просто игнорируется. После
рендеринга третьей поверхности ( тот же
DrawPrimitive, начальная вршина – 1088 ) поставишь
пресловутый DISABLE в COLOROP, тем самым просто
запретив второй слой.
Листинг №4, - main.cpp,
ф-я Render.
VOID Render()
{
//Очистка экрана и з-буфера
lpD3DD->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00, 1.0f, 0 );
//Начало рендеринга
lpD3DD->BeginScene();
//Рендеринг
примитивов
lpD3DD->SetTexture( 0, lpTexLin ); //Применение текстуры
lpD3DD->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 542 ); //Рендеринг
первой плоскости
lpD3DD->SetTexture( 0, lpTexRep );
lpD3DD->DrawPrimitive( D3DPT_TRIANGLESTRIP, 544, 542 ); //Рендеринг
второй плоскости
//Кульминация - мультитекстуры!
lpD3DD->SetTexture( 1, lpTexLin ); //Применение второй
текстуры
lpD3DD->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_ADD ); //Тип
получения цвета второй текстуры
lpD3DD->DrawPrimitive( D3DPT_TRIANGLESTRIP, 1088, 542 ); //
Рендеринг второй плоскости
lpD3DD->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_DISABLE ); //Отмена
рендеринга второй
текстуры
//Завершение рендеринга
lpD3DD->EndScene();
//Вывод на экран
lpD3DD->Present( NULL, NULL, NULL, NULL );
//Курсор - в зад
SetCursor( NULL );
}
Ну и последним
действием будет изменение функции UpdateMatrix.
Тут надо все удалить и добавить поворот
лампы вокруг оси Х, а потом – У.
Листинг №5, - main.cpp,
ф-я UpdateMatrix.
//Заполнение
матриц
VOID UpdateMatrix()
{
d3dLight.Position.y = -sinf( timeGetTime()/350.0f )*2+2; //Поворот
вокруг оси Х
d3dLight.Position.z = cosf( timeGetTime()/350.0f )*2;
d3dLight.Position.x = sinf( timeGetTime()/500.0f )*d3dLight.Position.z+2; //Поворот
вокруг оси У
d3dLight.Position.z = cosf( timeGetTime()/500.0f )*d3dLight.Position.z-2;
lpD3DD->SetLight( 0, &d3dLight ); //Применение лампы
}
Ну вот. Компиль и
запускай. У тебя получится что-то типа:
Если не работает,
то попробуй поменять тип девайса HAL на REF. Ну
что? Ну все. Бывай!
Happy rendering 4u!!!
ЗЫ Если ты хочешь
что-то конкретное увидеть в следующей
статье ( Bump-mapping, туман, например ), то мыль
мне, или здесь отзыв оставь.
ЗЗЫ Планируется
создание сайта, посвященного 3D графике.
Статьи об основах графики, D3D, OpenGL, Glide,
обзоры эффектов, железо, драйвера для
акселей и т.д. Если ты хочешь поделиться
своими знаниями в перечисленных выше
областях, то мыль мне.
Исходники: dxtexsrc.zip