Конничива! Сегодня, как я и обещал, у нас речь пойдет о текстурировании. Точнее о мультитекстурировании. Представьте что у вас есть карта уровня, на которую было бы неплохо добавить тени. Со стенсильными тенями у вас пока не сложилось, поэтому вы решаете схитрить и нарисовать их прямо поверх текстуры. Хорошенько обдумав этот вариант, вы решаете его оптимизировать – тени нарисовать на отдельной текстуре. Это неплохое решение, которое было весьма распространено (да и сейчас оно не теряет своей актуальности). Вот только для его реализации нам потребутся механизм накладывающий за один проход несколько текстур на полигон. Итак, встречайте – мультитекстурирование.

    Суть заключается в том что перед тем как треугольник будет растеризован и “закатан” в буфер кадра, его прошоняют по так называемым текстурным блокам. С каждым блоком связана текстура, парамтры её применения к треугольнику и способ фильтрации. Сначала к отрисовываемому объекту применяется первый текстурный блок (с индексом “0”), затем второй (с индексом “1”) и так далее. Самоей замечательное в том что нам не придется менять или дорабатывать код иницилизации текстур, всё что нам надо сделать так это инициализировать текстурные блоки. Делается это следующим образом:

    // указываем системе что все настройки, которые будут установлены далее,
    // точнее до следующего вызова glActiveTextureARB, будут относиться к первому 
    // текстурному блоку
    glActiveTextureARB( GL_TEXTURE0_ARB );
    // в первом текстурном блоке будет установлена 2D текстура
    glEnable( GL_TEXTURE_2D );
    // сосбственно хэндл текстуры
    glBindTexture( GL_TEXTURE_2D , TextureHandle );
    // параметр отображения, в данном примере мы сообщаем что нам не интересно какой
    // цвет был у текселя до применения этого текстурного блока, теперь у него будет 
    // цвет соответствующего текселя текстуры
    // если бы вы указали  GL_MODULATE, то получили бы 
    // текст полигона умноженного на цвет текселя текстуры
    glTexEnvi( GL_TEXTURE_ENV , GL_TEXTURE_ENV_MODE , GL_REPLACE );
	
    // ну тут всё аналогично – теперь работаем с первым текстуным блоком
    glActiveTextureARB( GL_TEXTURE1_ARB );
    // опять 2D текстура
    glEnable( GL_TEXTURE_2D );
    // накладываем вторую текстуру
    glBindTexture( GL_TEXTURE_2D , FilterHandle );
    // а вот теперь мы говорим что после применения второго текстурного блока
    // цвет текселя полигона рассчитывается как произведение текстуры второго блока
    // на цвет текселя полученного на предыдущем шаге
    glTexEnvi( GL_TEXTURE_ENV , GL_TEXTURE_ENV_MODE , GL_MODULATE );

    Изменения немного коснулись функций назначения текстурных координат. Теперь это делается с помощью функций glMultiTexCoord2fARB, где первый параметр это GL_TEXTURE0_ARB, GL_TEXTURE1_ARB и так далее. Код отрисовки квадрата будет следующим:

    glActiveTextureARB( GL_TEXTURE0_ARB );
    glBindTexture( GL_TEXTURE_2D , TextureHandle );

    glActiveTextureARB( GL_TEXTURE1_ARB );
    glBindTexture( GL_TEXTURE_2D , FilterHandle );

    glBegin( GL_QUADS );
	    glMultiTexCoord2fARB( GL_TEXTURE0_ARB , 0.0 , 1.0 );
	    glMultiTexCoord2fARB( GL_TEXTURE1_ARB , 0.0 , 1.0 );
	    glColor3f( 1 , 0 , 0 );glVertex3f( -1.0f , -1.0f , 0.0f );

	    glMultiTexCoord2fARB( GL_TEXTURE0_ARB , 0.0 , 0.0 );
	    glMultiTexCoord2fARB( GL_TEXTURE1_ARB , 0.0 , 0.0 );
	    glColor3f( 0 , 1 , 0 );glVertex3f( -1.0f ,  1.0f , 0.0f );

	    glMultiTexCoord2fARB( GL_TEXTURE0_ARB , 1.0 , 0.0 );
	    glMultiTexCoord2fARB( GL_TEXTURE1_ARB , 1.0 , 0.0 );
	    glColor3f( 0 , 0 , 1 );glVertex3f(  1.0f ,  1.0f , 0.0f );

	    glMultiTexCoord2fARB( GL_TEXTURE0_ARB , 1.0 , 1.0 );
	    glMultiTexCoord2fARB( GL_TEXTURE1_ARB , 1.0 , 1.0 );
	    glColor3f( 1 , 1 , 0 );glVertex3f(  1.0f , -1.0f , 0.0f );
    glEnd();

    glActiveTextureARB( GL_TEXTURE0_ARB );
    glBindTexture( GL_TEXTURE_2D , 0 );

    glActiveTextureARB( GL_TEXTURE1_ARB );
    glBindTexture( GL_TEXTURE_2D , 0 );

    Вот собственно и всё на сегодня. Если возникнут вопросы, то вы знаете када об этом можно написать.

    ЗЫ Исходники.

    Добрый вечер! Сегодняшний урок, как явственно следует из названия будет посвящен текстурированию. Что это за зверь? Давайте разбираться. Представьте, что у нас есть некий полигональный объект, которому отчаянно не хватает детализации. Какие будут ваши действия? Добавить ещё полигонов? Ну это не всегда возможно, т.к. увеличение детализации таким способом убьёт FPS. Поэтому и возникла идея часть деталей просто нарисовать на поверхности объекта. Именно в этом и заключается суть текстурирования – отрисовываемому объекту ставится в соответствие некий растр, в котором хранится изображение, накладываемое на полигоны. Вот вроде бы краткий брифинг завершен и можно приступать непосредственно к рассмотрению тонкостей процесса текстурирования в OpenGL'е.

    Поскольку OpenGL это вего лишь АПИ для рендеринга, то в нем отсутствуют почти все удобства, серьёно упрощающие жизнь тем кто избрал DirectX. Конкретно: в OpenGL нет функций позволяющих загружать изображения (конечно есть простенькие утилитарные библиотеки типа GLU или GLUT, но они особой погоды не дулают т.к. позволяют загружать максимум BMP файлы). Поэтому мы воспользуемся изученой ранее библиотекой DevIL (урок можно найти здесь). Поэтому с загрузки и начнем:

    CCommonBitmap::InitDevIL();
    CCommonBitmap::SetRenderer( ILUT_OPENGL );
    CCommonBitmap		Texture( "nvlogo.png" );

    Обращение к текстуре в OpenGL осуществляется через её идентификатор:

    unsigned int		TextureHandle;

    который перед использованием надо сгенерировать:

    glGenTextures( 1 , &TextureHandle );

    Затем надо сообщить системе что мы сейчас будем работать с текстурой с идентификатором TextureHandle:

    glBindTexture( GL_TEXTURE_2D , TextureHandle );

    Затем надо создать текстуру в видео памяти:

    gluBuild2DMipmaps( GL_TEXTURE_2D , 3 , Texture.GetInteger( 0 ) ,
		Texture.GetInteger( 1 ) , GL_RGB , GL_UNSIGNED_BYTE ,
		Texture.GetData() );

    Указать параметры фильтрации:

    glTexParameteri( GL_TEXTURE_2D , GL_TEXTURE_MIN_FILTER  , GL_LINEAR  );
    glTexParameteri( GL_TEXTURE_2D , GL_TEXTURE_MAG_FILTER , GL_LINEAR );

    Смысл этих параметров следующий – очень редко бывает когда одному пикселю экрана соответствует один пиксель текстуры. Как правило бывает, что одному пикселю экрана соответствует несколько текселей (GL_TEXTURE_MIN_FILTER) или одному текселю несколько пикселей (GL_TEXTURE_MAG_FILTER). Параметры фильтрации указывают системе каким образом рассчитывать цвет пикселя. В данном случае используется режим GL_LINEAR, т.е. цвет текселя усредняется в квадрате 2х2 текселя. Теперь нам только осталось сообщить системе что мы собираемся использовать текстурирование

    glEnable( GL_TEXTURE_2D );

и собственно осуществить отрисовку

    // привязываемся к текстуре
    glBindTexture( GL_TEXTURE_2D , TextureHandle );

    // рисуем геометрию
    glBegin( GL_QUADS );
	    glTexCoord2f( 0 , 1 );glColor3f( 1 , 1 , 1 );glVertex3f( -1.0f , -1.0f , 0.0f );
	    glTexCoord2f( 0 , 0 );glColor3f( 1 , 1 , 1 );glVertex3f( -1.0f ,  1.0f , 0.0f );
	    glTexCoord2f( 1 , 0 );glColor3f( 1 , 1 , 1 );glVertex3f(  1.0f ,  1.0f , 0.0f );
	    glTexCoord2f( 1 , 1 );glColor3f( 1 , 1 , 1 );glVertex3f(  1.0f , -1.0f , 0.0f );
    glEnd();

    // отвязываемся от текстуры
    glBindTexture( GL_TEXTURE_2D , 0 );

    Вот собственно и всё на сегодня. Однако тему текстурирования рано закрывать, т.к. по ней планируется сделать ещё как минимум 2 статьи. Следите за обновлениями )).

    ЗЫ Исходники.

    Как вы наверное могли уже понять нам понадобятся кватернионы и классы для работы с DirectInput'ом (статьи по этим темам можно посмотреть на этом сайте в разделах Алгоритмы и DirectX->DirectInput8 соответственно). Поэтому приступим сразу к реализации камеры.

    Первым делом нам надо настроить сопособ проецирования (между ортогональной проекцией и перспективной мы ни минуты не сомневаясь выберем последнюю). Делается это так:

	//выбираем матрицу которую будем устанавливать
	glMatrixMode( GL_PROJECTION );
	//загружаем единичную матрицу
	glLoadIdentity();
	//устанавливаем перспективную проекцию с углом обзора в 60 градусов
	//ближней плоскостью отсечения на расстоянии 1 и дальней на расстоянии 1000
	//здесь же 1.33 – это отношение ширины экрана к высоте (800/600)
	gluPerspective( 60 , 1.33 , 1 , 1000 );

    Как вы видите мы здесь использовали функцию gluPerspective, которая как видно про префиксу «glu» является частью библиотеки утилит. Для того чтобы компиляция прошла без ошибок, вам надо будет подключить файл glu.h и либку glu32.lib. Например так:

#pragma		comment ( lib , "glu32.lib" )

    Реализация класса камеры.

class Camera{
public:
	Vector4	target , norm , eye;
	Camera( void ){}
	Camera( Vector4 e , Vector4 n , Vector4 t ){
		eye = e;
		norm = n;
		target = t;
	}

	//basik movement functions
	void    MoveLeft( void );
	void    MoveRight( void );
	void    MoveForward( void );
	void    MoveBackward( void );
	void	MoveUp( void );
	void	MoveDown( void );

	//input functions
	void	KeyboardInput( Keyboard & );
	void	MouseInput( Mouse & , int , int );

	//init functions
	void	ReInitCamera( void );
	void	InitCamera( Vector4 , Vector4 , Vector4 );

	//camera rotation functions
	void	SpinAlongY( float );//Y - norm
};

    Здесь поля target , norm , eye - это соответственно вектор направления взгляда, вектор вертикали вида, и координаты точки визирования. Для облегчения восприятия рекомендую обратить свой взор на рисунок:

    Теперь быстренько пробежимся по методам класса. «Быстренко» потому что они до безобразия простые.

void	Camera::MoveUp( void ){
	eye += CAM_VELOSITY * norm;
}

void	Camera::MoveDown( void ){
	eye -= CAM_VELOSITY * norm; 
}

void    Camera::MoveLeft( void ){ 
	eye += CAM_VELOSITY * ( norm * target );
}

void    Camera::MoveRight( void ){
	eye += CAM_VELOSITY * ( target * norm );
}

void    Camera::MoveForward( void ){
	eye += CAM_VELOSITY * target;
}

void    Camera::MoveBackward( void ){
	eye -= CAM_VELOSITY * target;
}

void	Camera::KeyboardInput( cKeyboard &Keyboard ){
	Keyboard.GetKeyboardState();

	if( Keyboard.GetButtonState( DIK_W ) )MoveForward();
	if( Keyboard.GetButtonState( DIK_S ) )MoveBackward();
	if( Keyboard.GetButtonState( DIK_A ) )MoveLeft();
	if( Keyboard.GetButtonState( DIK_D ) )MoveRight();

	ReInitCamera();
}

void	Camera::ReInitCamera( void ){
	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();
	gluLookAt(	eye.x , eye.y , eye.z , 
		eye.x + target.x , eye.y + target.y , eye.z + target.z , 
		norm.x , norm.y , norm.z );
}

void	Camera::InitCamera( cVector4 EYE , cVector4 TARGET , cVector4 NORM ){
	eye	= EYE;
	target	= TARGET;
	norm	= NORM;

	ReInitCamera();
}

void	Camera::SpinAlongX( float Angle ){
	cQuaternion		ry;
	ry.fRy( Angle );
	target = ry * target;
}

void	Camera::MouseInput( cMouse &Mouse , int SCREEN_WIDTH , int SCREEN_HEIGHT ){
	float	x( Mouse.GetOffset( 0 ) );
	float	AngleX( ( ( float )  -x ) / 300 );
	SpinAlongX( AngleX );
	SetCursorPos( SCREEN_WIDTH / 2 , SCREEN_HEIGHT / 2 );
	ReInitCamera();
}

    Здесь CAM_VELOSITY – это некоторая константа, которая хранит скорость перемещения камеры. Далее:

	//сообщаем системе, что сейчас будем менять матрицу вида
	glMatrixMode( GL_MODELVIEW ); 
	glLoadIdentity();
	//передаем параметры положения камеры
	gluLookAt(	eye.x , eye.y , eye.z , 
		eye.x + target.x , eye.y + target.y , eye.z + target.z , 
		norm.x , norm.y , norm.z );

    Вот вроде и все. Просто да?

    Исходники можно скачать здесь.

    До сих пор для осуществления преобразований мы пользовались либо только достаточно простыми механизмами (сдвиг вектора), либо механизмами, которые нельзя применять повсеместно (кватернионы). Конечно у нас есть достаточно навороченный класс cVector, с помощью которого можно сдвигать и вращать вершины. Однако все эти преобразования будут выполняться на CPU и, следовательно, существенно тормозить нашу игру. У кватернионов те же недостатки. Поэтому сейчас настало время рассмотреть способ, с помощью которого можно осуществлять все преобразования практически даром!

    Речь пойдет о матричных преобразованиях, которые предоставляет нам в наше распоряжение OpenGL. В этой графической библиотеке реализовано три типа матриц: матрица проецирования, матрица вида, текстурная матрица. Сегодня речь пойдет только о матрице вида.

    Все эти три матрицы можно свободно изменять. Для начала нужно сообщить системе с каком матрицей мы будем работать. Сделать это можно вызвав функцию glMatrixMode:

    glMatrixMode( mode );
    // где mode это одно из следующих значений
    // GL_MODELVIEW – матрица вида
    // GL_PROJECTION – матрица проецирования
    // GL_TEXTURE – текстурная матрица

    С матрицей проецирования и текстурной матрицей все более менее понятно – они отвечают за проецирование (ортогональное, перспективное etc.) и модификацию текстурных координат. А вот матрица вида требует отдельного, более подробного объяснения. Дело в том что при рендеринге все вершины попадают на конвейер рендеринга в так называемом camera space'e. Это такое пространство, в котором ось OZ направлена в сторону противоположной направлению взгляда (ну и с помощью нормали вида достраиваются оси OX и OY), с началом координат в точке визирования (там где расположена камера). Поэтому если перед рендерингом поставить матрицу вида следующим образом:

    glMatrixMode( GL_MODELVIEW );

    // загрузка единичной матрицы
    glLoadIdentity();

    то вся выводимая геометрия будет выводиться непосредственно перед экраном а не там где ей положено быть. Для перевода координат геометрии из camera space'а в глобальные координаты, как раз и применяется матрица вида.

    После этого можно выбранную матрицу домножать справа на матрицы поворота, смещения или переноса:

    // поворот на угол angle (задается в градусах)
    // вокруг вектора ( vx , vy , vz )
    glRotatef( angle , vx , vy , vz );

    // перенос на вектор ( tx , ty , tz )
    glTranslatef( tx , ty , tz );

    // масштабирование по осям x, y, z
    // sx, sy, sz – масштабирующие коэффициенты 
    // по соответствующим осям
    glScalef( sx , sy , sz );

    К сожалению эти функции изменяют выбранную матрицу. Поэтому после окончания преобразований нужно возвращать измененную матрицу в исходное состояние. Можно было бы конечно высчитывать обратные матрицы, для выполнения обратных преобразований, но OpenGL предлагает другой, более простой и элегантный способ – стек матриц. Работа с ним осуществляется следующим образом – перед началом преобразований сохраняем матрицу в стеке, которую планируем изменять. Изменяем матрицу согласно нашим замыслам. Выполняем преобразование. Выталкиваем исходную матрицу из стека. Все просто! Работа со стеком осуществляется с помощью следующих функций:

    // помещение матрицы в стэк
    glPushMatrix();

    // выталкивание матрицы из стека
    glPopMatrix();

    При использовании преобразований следует помнить следующее правило: вектор домножается на матрицу справа, т.е. при перемножении справа стоит вектор, а слева – матрица. Таким образом, если у нас есть последовательность преобразований, то последовательность функций должна быть противоположной. Для большей ясности рассмотрим один пример: пускай у нас есть квадрат, центр которого расположен в начале координат. Нам надо этот квадрат поворачивать вокруг собственного центра, а затем, отодвинув его от начала координат на 3 единицы, повернуть его вокруг начала координат, ну и для большего интереса будем масштабировать этот квадрат. В качестве решения задачи имеем следующую последовательность преобразований:
1. масштабировать квадрат
2. повернуть квадрат вокруг своего центра
3. сдвинуть квадрат на 3 единицы
4. повернуть квадрат вокруг начала координат

    Теперь распишем вызовы соответствующих функций:

    glMatrixMode( GL_MODELVIEW );

    // вращение вокруг начала координат
    glRotatef( Angle , 0 , 0 , 1 );

    //сдвиг на 3 единицы
    glTranslatef( 3 , 0 , 0 );

    //вращение вокруг центра квадрата
    glRotatef( Angle++ , 0 , 0 , 1 );

    //масштабирование
    glScalef( Scale , Scale , 0 );

    Как видите, все согласно правилу.

    Вот пожалуй и все на сегодня, вроде все просто, однако если возникнут вопросы – пишите сами знаете куда )).

    ЗЫ исходные коды к данной статье прилагаются.

В предыдущем уроке был рассмотрен каркас простейшего win32 приложения. Там же были причины почему нас не удовлетворят возможности glut'а и почему нужно ваять код инициализации самостоятельно. Посему без лишних реверансов приступим к реализации. Основа всего – класс посредством которого будет осуществляться инициализация OpenGL'я:
class		OGLDevice{
	//контекст воспроизведения OpenGL
	HGLRC			hGLrc;
public:
	//функция инициализации
	void			CreateDevice( HWND hWnd );
	//очевидно ))
	void			Release( void );
	//функция переключения буферов
	void			SwapBuffers( void );
};

void			OGLDevice::CreateDevice( HWND hWnd ){
	PIXELFORMATDESCRIPTOR	pfd;
	memset( &pfd , 0 , sizeof( PIXELFORMATDESCRIPTOR ) );
	DEVMODE			devmode;
	memset( &devmode , 0 , sizeof( devmode ) );

	pfd.nSize		=	sizeof(PIXELFORMATDESCRIPTOR);
	//версия структуры  PIXELFORMATDESCRIPTOR
	pfd.nVersion		=	1;
	pfd.dwFlags		=	PFD_DRAW_TO_WINDOW | //вывод в окно
				PFD_SUPPORT_OPENGL | //включена поддержка OGL
				PFD_DOUBLEBUFFER; //двойная буферизация
	pfd.iPixelType	=	PFD_TYPE_RGBA;//формат задания цвета
	pfd.cColorBits	=	32;//глубина цвета в битах
	pfd.cDepthBits	=	32;//разрядность z-буфера
	pfd.iLayerType	=	PFD_MAIN_PLANE;
	
	//находим индекс формата пикселов
	int			PixelFormat = ChoosePixelFormat( GetDC( hWnd ) , &pfd );

	if( PixelFormat == 0 ){
		PixelFormat = 1;
		//находим описатель формата пикселов
		if( DescribePixelFormat( GetDC( hWnd ) , PixelFormat , sizeof( PIXELFORMATDESCRIPTOR ) , &pfd ) == 0 ){
			exit( 0 );
		}
	}

	//устанавливаем формат пикселов
	if( SetPixelFormat( GetDC( hWnd ) , PixelFormat , &pfd  ) == NULL ){
		exit( 0 );
	}

	//создаем контекст
	if( ( hGLrc = wglCreateContext( GetDC( hWnd ) ) ) == NULL ){
		exit( 0 );
	}

	//делаем созданный контекст текущим
	if( wglMakeCurrent( GetDC( hWnd ) , hGLrc ) == false ){
		exit( 0 );
	}

	//осуществляем переход в полноэкранный режим
	if( !EnumDisplaySettings( NULL , ENUM_CURRENT_SETTINGS , &devmode ) ){
		exit( 0 );
	}
	devmode.dmSize = sizeof( DEVMODE );
	devmode.dmPelsWidth		= 800;
	devmode.dmPelsHeight	= 600;
	devmode.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;
	if( ChangeDisplaySettings( &devmode , CDS_FULLSCREEN ) != DISP_CHANGE_SUCCESSFUL ){
		exit( 0 );
	}
	
	ShowWindow( hWnd , SW_SHOW );
	SetForegroundWindow( hWnd );
	SetFocus( hWnd );
}
Теперь при разработке своих программ вам надо будет определить объект для инициализации:
OGLDevice		*Device;
Device = new OGLDevice;
Device->CreateDevice( hWnd );
//здесь hWnd – это хендл окна в которое осуществляется вывод
Оставшиеся две функции достаточно просты:
void			OGLDevice::SwapBuffers( void ){
	//переключаем буфер
	::SwapBuffers( wglGetCurrentDC() );
	//устанавливаем цвет которым будет очищаться буфер кадра
	glClearColor( 0 , 0 , 0.5 , 0 );
	//очищаем буферы
	//в данном примере очищаются буфер кадра и z-буфер
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
}

//функция уничтожения OGL окна, тоже в общем-то ничего сложного
void			OGLDevice::Release( void ){
	if( wglGetCurrentContext() != NULL ){
		wglMakeCurrent( NULL , NULL );
	}
	if( hGLrc != NULL ){
		//удаление контекста
		wglDeleteContext( hGLrc );
		hGLrc = NULL;
	}
}
Вот собственно и все.

Исходники можно скачать здесь.