Этот урок был написан после нескольких e-mail-ов с вопросами о том, каким образом можно наложить текстуру на поверхность. И хоть это и очень просто, я решил всё-таки написать этот урок. Также я несколько изменил код предыдущего урока, чтобы поверхность вместо квадратов строилась из последовательности треугольников (triangle strip). В следующем уроке мы пойдем дальше — рассмотрим, как детализировать текстуры поверхности для большего реализма, а не просто накладывать одну повторяющуюся. Также стырим немного кода из урока по созданию скайбокса, чтобы в нашей сцене был симпатичный фон и она выглядела пореальней.

Текстуры поверхности созданы в Terragen.
Их размер — 512х512, так что если ваша видеокарта не держит такие большие текстуры, уменьшите их до 256 или 128. То же самое относится к текстурам поверхности.

Функционал камеры мы возьмем из последнего урока камеры.
Кроме того, добавим без изменений наш класс текстур.

В файле camera.cpp добавим возможность передвигаться по вертикали:

void CCamera::MoveCamera(float speed)
{
CVector3 vVector = m_vView m_vPosition;
vVector = Normalize(vVector);m_vPosition.x += vVector.x * speed;
m_vPosition.y += vVector.y * speed;
m_vPosition.z += vVector.z * speed;
m_vView.x += vVector.x * speed;
m_vView.y += vVector.y * speed;
m_vView.z += vVector.z * speed;
}

 

Расширим функционал отрисовки поверхности:

Файл terrain.h:

// Изменим прототип RenderHeightMap():
void RenderHeightMap(BYTE *pHeightMap,  GLuint TID);

 

Файл terrain.cpp:

///////////////////////////////// SET TEXTURE COORD \\\\\\\\\\\\\\\\*
/////
/////   Новая функция: устанавливает текстурные координаты поверхности на основе X и Z
/////
///////////////////////////////// SET TEXTURE COORD \\\\\\\\\\\\\\\\*void SetTextureCoord(float x, float z)
{
// Нам нужно рассчитать текстурные координаты для текущей вершины. Чтобы
// сделать это, просто берём текущие X & Y, и делми их на MAP_SIZE нашей поверхности.
// Конечно, это подразумевает, что карта высот имеет такие же размеры, что и текстура.// Give OpenGL the current texture coordinate for our height map
glTexCoord2f(   (float)x / (float)MAP_SIZE,
(float)z / (float)MAP_SIZE  );
}

///////////////////////////////// RENDER HEIGHT MAP «»»»»»»»»»\*
/////
/////   Переписываем функцию.
/////
///////////////////////////////// RENDER HEIGHT MAP «»»»»»»»»»\*

void RenderHeightMap(BYTE pHeightMap[], GLuint TID)
{
int X = 0, Y = 0;       // Переменные для прохда по массиву
int x, y, z;            // Переменные для чтения
bool bSwitchSides = false;

if(!pHeightMap) return;     // Убедимся, что данные верны

// Различие между прошлым способом рендера поверхности и этим в том,
// что мы больше не используем GL_QUADS, а вместо них возьмём GL_TRIANGLE_STRIP.
// Это означает, что нам больше не нужно передавать одну и ту же вершину несколько раз.
// Каждые 2 вершины соединяются со следующими двумя. Так как мы хотим
// сделать всё за один цикл, нам нужно переворачивать порядок вершин каждый ряд.
// Доходим до конца и разворачиваемся. Мы могли бы использовать glBegin
// и glEnd для каждого ряда, но этот способ работает быстрее. Учтите,
// что рендер поверхности вызовами glVertex*() ОЧЕНЬ медленный. Вообще,
// лучше бы использовать для таких целей массивы вершин.
//

// Биндим текстуру
glBindTexture(GL_TEXTURE_2D, TID);

// Мы хотим рендерить последовательность треугольников
glBegin( GL_TRIANGLE_STRIP );

// Проходим через все ряды карты вершин
for ( X = 0; X <= MAP_SIZE; X += STEP_SIZE )
{
// Проверяем, нужно ли рендерить в противоположную сторону
if(bSwitchSides)
{
// Рендерим линию поверхности для текущего X.
// Начинаем с MAP_SIZE и идём до 0.
for ( Y = MAP_SIZE; Y >= 0; Y -= STEP_SIZE )
{
// Получаем (X, Y, Z) значения для нижней левой вершины
x = X;
y = Height(pHeightMap, X, Y );
z = Y;

// Текстурные координаты для вершины
SetTextureCoord( (float)x, (float)z );
glVertex3i(x, y, z);

// Получаем (X, Y, Z) для нижней правой вершины
x = X + STEP_SIZE;
y = Height(pHeightMap, X + STEP_SIZE, Y );
z = Y;

// Устанавливаем текстурные координаты и рендерим вершину
SetTextureCoord( (float)x, (float)z );
glVertex3i(x, y, z);
}
}
else
{
// Рендерим линию поверхности для текущего X.
// Начинаем с 0 и идём до MAP_SIZE.
for ( Y = 0; Y <= MAP_SIZE; Y += STEP_SIZE )
{
// Получаем (X, Y, Z) для нижней правой вершины
x = X + STEP_SIZE;
y = Height(pHeightMap, X + STEP_SIZE, Y );
z = Y;

// Устанавливаем текстурные координаты и рендерим вершину
SetTextureCoord( (float)x, (float)z );
glVertex3i(x, y, z);

// Получаем (X, Y, Z) значения для нижней левой вершины
x = X;
y = Height(pHeightMap, X, Y );
z = Y;

// Устанавливаем текстурные координаты и рендерим вершину
SetTextureCoord( (float)x, (float)z );
glVertex3i(x, y, z);
}
}

// Меняем направление рендера
bSwitchSides = !bSwitchSides;
}

glEnd();
}

 

Изменений не много, но теперь у нас есть симпатичная сетка поверхности! =)

Теперь перейдём к main.cpp:

// В начале — глобальные переменные:
CCamera g_Camera;CTexture *Texture;TextureImage textures[7];

BYTE g_HeightMap[MAP_SIZE*MAP_SIZE];

bool  g_bRenderMode = true;

// ID текстур для сторон скайбокса:
#define BACK_ID     1
#define FRONT_ID    2
#define BOTTOM_ID   3
#define TOP_ID      4
#define LEFT_ID     5
#define RIGHT_ID    6

///////////////////////////////// CREATE SKY BOX \\\\\\\\\\\\\\\\*
/////
/////   Создаёт текстурированный скайбокс
/////
///////////////////////////////// CREATE SKY BOX \\\\\\\\\\\\\\\\*

void CreateSkyBox(float x, float y, float z, float width, float height, float length)
{
// Включаем текстуры
glEnable(GL_TEXTURE_2D);

// Задняя текстура — к задней стороне куда
glBindTexture(GL_TEXTURE_2D, textures[BACK_ID].texID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

// Устанавливаем центр в x/y/z
x = x width  / 2;
y = y height / 2;
z = z length / 2;

// Рисуем сторону
glBegin(GL_QUADS);

// текстурные координаты и вершины задней стороны
glTexCoord2f(1.0f, 0.0f); glVertex3f(x + width, y,z);
glTexCoord2f(1.0f, 1.0f); glVertex3f(x + width, y + height, z);
glTexCoord2f(0.0f, 1.0f); glVertex3f(x, y + height, z);
glTexCoord2f(0.0f, 0.0f); glVertex3f(x, y,z);

glEnd();

// Передняя текстура к передней стороне
glBindTexture(GL_TEXTURE_2D, textures[FRONT_ID].texID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

// Рисуем сторону
glBegin(GL_QUADS);

// Текстурные координаты и вершины передней стороны
glTexCoord2f(1.0f, 0.0f); glVertex3f(x, y,z + length);
glTexCoord2f(1.0f, 1.0f); glVertex3f(x, y + height, z + length);
glTexCoord2f(0.0f, 1.0f); glVertex3f(x + width, y + height, z + length);
glTexCoord2f(0.0f, 0.0f); glVertex3f(x + width, y,z + length);
glEnd();

// Нижняя текстура на нижнюю сторону
glBindTexture(GL_TEXTURE_2D, textures[BOTTOM_ID].texID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

// Рисуем сторону
glBegin(GL_QUADS);

// Текстурные координаты и вершины нижней стороны
glTexCoord2f(1.0f, 0.0f); glVertex3f(x, y,z);
glTexCoord2f(1.0f, 1.0f); glVertex3f(x, y,z + length);
glTexCoord2f(0.0f, 1.0f); glVertex3f(x + width, y,z + length);
glTexCoord2f(0.0f, 0.0f); glVertex3f(x + width, y,z);
glEnd();

// Верхняя текстура на верхнюю сторону
glBindTexture(GL_TEXTURE_2D, textures[TOP_ID].texID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

// Рисуем сторону
glBegin(GL_QUADS);

// Текстурные координаты и вершины верхней стороны
glTexCoord2f(0.0f, 1.0f); glVertex3f(x + width, y + height, z);
glTexCoord2f(0.0f, 0.0f); glVertex3f(x + width, y + height, z + length);
glTexCoord2f(1.0f, 0.0f); glVertex3f(x, y + height,z + length);
glTexCoord2f(1.0f, 1.0f); glVertex3f(x, y + height,z);

glEnd();

// Левая текстура на левую сторону
glBindTexture(GL_TEXTURE_2D, textures[LEFT_ID].texID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

// Рисуем сторону
glBegin(GL_QUADS);

// Текстурные координаты и вершины
glTexCoord2f(1.0f, 1.0f); glVertex3f(x, y + height,z);
glTexCoord2f(0.0f, 1.0f); glVertex3f(x, y + height,z + length);
glTexCoord2f(0.0f, 0.0f); glVertex3f(x, y,z + length);
glTexCoord2f(1.0f, 0.0f); glVertex3f(x, y,z);

glEnd();

// Текстура правой стороны
glBindTexture(GL_TEXTURE_2D, textures[RIGHT_ID].texID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

// Рисуем сторону
glBegin(GL_QUADS);

// Текстурные координаты и вершины
glTexCoord2f(0.0f, 0.0f); glVertex3f(x + width, y,z);
glTexCoord2f(1.0f, 0.0f); glVertex3f(x + width, y,z + length);
glTexCoord2f(1.0f, 1.0f); glVertex3f(x + width, y + height,z + length);
glTexCoord2f(0.0f, 1.0f); glVertex3f(x + width, y + height,z);
glEnd();
}

////////////////////////////////////////////////////////////////////////////////////////
//
// Изменяем инициализацию:
//

void Init(HWND hWnd)
{
g_hWnd = hWnd;
GetClientRect(g_hWnd, &g_rRect);
InitializeOpenGL(g_rRect.right, g_rRect.bottom);

LoadRawFile(«Terrain.raw», MAP_SIZE * MAP_SIZE, g_HeightMap);

glEnable(GL_DEPTH_TEST);        // Тест глубины
glEnable(GL_TEXTURE_2D);        // Текстуры
glEnable(GL_CULL_FACE);         // Обрезка задних полигонов

// Загружаем нужные текстуры:
Texture = new CTexture();
Texture->LoadTexture(IL_BMP,«Terrain.bmp», &textures[0]);
Texture->LoadTexture(IL_BMP,«Back.bmp», &textures[BACK_ID]);
Texture->LoadTexture(IL_BMP,«Front.bmp», &textures[FRONT_ID]);
Texture->LoadTexture(IL_BMP,«Bottom.bmp», &textures[BOTTOM_ID]);
Texture->LoadTexture(IL_BMP,«Top.bmp», &textures[TOP_ID]);
Texture->LoadTexture(IL_BMP,«Left.bmp», &textures[LEFT_ID]);
Texture->LoadTexture(IL_BMP,«Right.bmp», &textures[RIGHT_ID]);

g_Camera.PositionCamera( 280, 35, 225,  281, 35, 225,  0, 1, 0);

}

////////////////////////////////////////////////////////////////////////////////////////
//
// Отрисовка:
//

void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();

g_Camera.Look();

// Рендерим карту высот
RenderHeightMap(g_HeightMap, textures[0].texID);

// Просто для лучшего эффекта я добавил скайбокс, окружающий поверхность,
// и проверку, не проходит ли камера сквозь поверхность. Проверка очень проста —
// это не коллизии из прошлых уроков. Просто проверяем высоту камеры и текущие
// координаты x/z. Если камера ниже поверхности, поднимаем её вверх.
// Настоящую обработку коллизий добавим в следующих уроках.

// Создадим скайбокс
CreateSkyBox(500, 0, 500, 2000, 2000, 2000);

// Получаем текущую позицию камеры
CVector3 vPos   = g_Camera.Position();
CVector3 vNewPos    = vPos;

// Проверяем высоту камеры для текущей позиции x/z, и прибавляем 10,
// чтобы камера не была прямо на полу.
if(vPos.y < Height(g_HeightMap, (int)vPos.x, (int)vPos.z ) + 10)
{
// Установим новую позицию камеры = высота поверхности + 10
vNewPos.y = (float)Height(g_HeightMap, (int)vPos.x, (int)vPos.z ) + 10;

// разница позиции камеры, прошлой и новой
float temp = vNewPos.y vPos.y;

//  Прибавим к вектору взгляда эту разницу
CVector3 vView = g_Camera.View();
vView.y += temp;

// Установим камеру в новую позицию
g_Camera.PositionCamera(vNewPos.x,  vNewPos.y,  vNewPos.z,
vView.x,    vView.y,    vView.z,    0, 1, 0);
}

SwapBuffers(g_hDC);
}

//////////////////////////////////////////////////////////////////////////////////////////
//
// Обработка событий (щелчек мыши):

case WM_LBUTTONDOWN:

g_bRenderMode = !g_bRenderMode;

if(g_bRenderMode)
{
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
else
{
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
}
break;

 

В файле init.cpp увеличим видимость:

    gluPerspective(45.0f,(GLfloat)width/(GLfloat)height, 4 ,4000.0f);

 

И наконец в начале main.h добавьте:

#include «texture.h»
#include «terrain.h»

 

Если вы хотите создать свой собственный .raw файл, это можно сделать с помощью фотошопа.
Например, выберите Render->Clouds и сохраните результат в формате .raw
Или вы можете создать свой собственный код поверхности, а затем fwrite() байты в .raw-файл.

Весь арт для урока создан в программе Terragen.
Она просто великолепна!

Update!
Если не будет компилироватся с ошибкой вроде

main.cpp(134) : error C2065: ‘GL_CLAMP_TO_EDGE’ : undeclared identifier
main.cpp(135) : error C2065: ‘GL_CLAMP_TO_EDGE’ : undeclared identifier

 

Добавьте в начало main.cpp:

#ifndef GL_CLAMP_TO_EDGE
#define GL_CLAMP_TO_EDGE        0x812F
#endif

Исходные коды к уроку