NeHe Tutorials Народный учебник по OpenGL
Урок 42. OpenGL

Multiple Viewports

 

Урок 42. Множественные области просмотра

 

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

 

После того как вы поймете материал урока, создание многооконных игр и 3-мерных приложений с несколькими областями просмотра будет для вас понятным и простым! Теперь давайте перейдем к коду.

 

Вы можете использовать самый свежий код NeHeGL или код Ipicture в качестве каркаса урока. Первый файл, на который мы обратим внимание это NeHeGL.cpp. Три раздела в коде были изменены. Я буду говорить только об измененном коде.

 

Первое и наиболее важное изменение сделано в ReshapeGL( ). Именно здесь мы задаем размеры экрана (наша основная область просмотра). Все настройки основного экрана выполнены в основном цикле.

 

void ReshapeGL (int width, int height)               

// перерисовать, когда окно двигается или изменяет размер

{

  // восстановить текущую область просмотра

  glViewport (0, 0, (GLsizei)(width), (GLsizei)(height));

}

 

Следующим шагом добавляем код для слежения за сообщением о стирании фонового изображения (WM_ERASEBKGND). Если это сообщение пришло, то мы его перехватываем и возвращаем 0. Это предотвратит стирание нашего фона и позволит изменить размер основного окна без всяких  неприятных мельканий, которые обычно возникают. Если вам непонятно что я имею в виду, просто уберите строчки “case WM_ERASEBKGND: и return 0;” и сразу увидите разницу.

 

// обрабатываем сообщения Windows

LRESULT CALLBACK WindowProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

  DWORD    tickCount;   // значение текущего счетчика времени

__int64    timer;     // используется для счетчика времени

 

  // получаем экземпляр Window

  GL_Window* window = (GL_Window*)(GetWindowLong (hWnd, GWL_USERDATA));

 

  switch (uMsg)     // проверяем сообщение Windows

  {

    case WM_ERASEBKGND: // если Windows стараются стереть фоновое изображение

      // возвращаем 0 (убираем эффект мелькания при изменении размера окна)

      return 0;

 

В WinMain нам надо изменить название окна и увеличить разрешение до 1024x768. Если ваш монитор по каким-то причинам не поддерживает 1024x768, вы можете уменьшить разрешение, но при этом будут теряться детали.

 

// основной вход в программу (WinMain)

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

{

  Application      application;  // структура приложения

  GL_Window      window;         // структура Window

  Keys        keys;              // структура клавиатуры

  BOOL        isMessagePumpActive; // Активирована передача сообщений?

  MSG        msg;              // структура сообщений Window

   // заполняем данные приложения

   application.className = "OpenGL"; // имя класса приложения

   application.hInstance = hInstance;  // экземпляр приложения

 

   // заполняем окно

   ZeroMemory (&window, sizeof (GL_Window)); // обнуляем память

   window.keys = &keys;                 // структура клавиатуры Window

   window.init.application = &application;  // приложение окна

 

  // название окна

  window.init.title = "Lesson 42: Multiple Viewports... 2003 NeHe Productions... Building Maze!";

 

  window.init.width = 1024;       // ширина окна

  window.init.height = 768;       // высота окна

  window.init.bitsPerPixel = 32;    // битов на пиксель

  window.init.isFullScreen = TRUE;  // полный экран? (ставим в TRUE)

 

Настало время сделать изменения в файле lesson42.cpp (основной код) ….

 

Начнем с добавления стандартных заголовочных файлов и файлов библиотек.

 

#include <windows.h>    // заголовочный файл для Windows

#include <gl\gl.h>      // заголовочный файл для OpenGL32 библиотеки

#include <gl\glu.h>     // заголовочный файл для GLu32 библиотеки

 

#include "NeHeGL.h"     // заголовочный файл для NeHeGL

 

#pragma comment( lib, "opengl32.lib" ) // ищи OpenGL32.lib при линковании

#pragma comment( lib, "glu32.lib" )    // ищи GLu32.lib при линковании

 

GL_Window*  g_window;               // структура Window

Keys*    g_keys;                    // клавиатура

 

Затем зададим все глобальные переменные, которые нам понадобятся в программе.

 

Переменные mx и my содержат информацию о комнате лабиринта, в которой мы находимся. Каждая комната разделена стеной (поэтому комнаты разнесены на 2 единицы).

 

Переменные width и height используются для построения нашей текстуры. Они также означают ширину и высоту нашего лабиринта. Причина, по которой мы выбрали лабиринт и текстуру одинакового размера, является тот, что каждый пиксель в лабиринте соответствует одному пикселю в текстуре. Мне нравится задавать ширину и высоту величиной 256, хотя это и потребует большего времени для построения лабиринта.

 

Если видеокарта на вашем компьютере может работать с текстурами большего размера, то попробуйте увеличить ее размер в степени 2 (256, 512, 1024). Проследите, чтобы значение не было слишком большим. Если размер основного окна 1024 пикселей и каждая область просмотра составляет половину основного окна, то максимум по ширине текстура будет иметь размер: Ширина Окна / 2. Поэтому если вы сделаете текстуру шириной 1024 пикселей, а область просмотра составит всего 512, то каждый второй пиксель будет перекрываться, т.к. места для размещения всех пикселей текстуры в области просмотра не хватит.

 

// переменные пользователя

int  mx,my;               // основной цикл (используются для поиска)

 

const  width  = 128;      // ширина лабиринта  (в степени двойки)

const  height  = 128;     // высота лабиринта  (в степени двойки)

 

Переменная done будет использована как сигнал об окончании построения лабиринта. Подробности ниже.

 

Переменные sp используется для проверки удержания нажатой клавиши Пробел. Нажатием этой клавиши лабиринт будет сброшен и программа нарисует новый. Если эту проверку не делать, то лабиринт будет сбрасываться много раз подряд, когда нажата клавиша Пробел. Эта переменная делает возможным сброс лабиринта только один раз.

 

BOOL  done;                  // флаг окончания

BOOL  sp;                    // нажата клавиша Пробел?

 

Массив r[4] содержит 4 случайных значения для красного, g[4] – для зеленого и b[4] – для голубого цветов. Эти значения будут использованы для задания различных цветов для каждой области просмотра. Первая область просмотра будет иметь цвет r[0],g[0],b[0]. Заметьте, что каждый цвет имеет значение одного байта, а не вещественное значение, к которому многие из вас привыкли. Причиной, по которой я использую именно байт, является тот факт, что гораздо легче присвоить случайное значение из диапазона 0-255, чем значение из диапазона 0.0f-1.0f.

 

Переменная tex_data – это указатель на наши данные текстуры.

 

BYTE  r[4], g[4], b[4]; // случайные цвета (4 красный, 4 зеленный, 4 синий)

BYTE  *tex_data;      // содержит данные нашей текстуры

 

Переменные xrot, yrot и zrot будут использованы для вращения наших трехмерных обьектов.

 

Наконец мы задаем квадратичный обьект, в котором позже мы можем нарисовать цилиндр и сферу используя gluCylinder и gluSphere. Это значительно легче, чем отрисовывать обьект вручную.

 

GLfloat  xrot, yrot, zrot;    // используются для вращения обьекта

GLUquadricObj *quadric;       // квадратичный обьект

 

Следующая порция кода зарисовывает пиксель текстуры (тексель) в точке с координатами dmx, dmy в чистый белый цвет. Переменная tex_data – это указатель на данные нашей структуры. Каждый пиксель задается тремя байтами (один байт для красного, зеленого и синего цветов). Смещение для красного составляет 0. Местоположение пикселя, который мы хотим изменить, подсчитывается как сумма dmx (положение по х), dmy (положение по у), умноженная на ширину нашей текстуры, с конечным результатом умноженным на 3 (3 байта на каждый пиксель).

 

В первой строке, приведенной ниже, в красный цвет (0) заносится значение 255. Во второй строке в зеленый цвет (1) заносится 255, то же для синего цвета (2). В результате получается пиксель белого цвета с координатами dmx, dmy.

 

void UpdateTex(int dmx, int dmy) // зарисовать пиксель dmx, dmy на текстуре

{

  tex_data[0+((dmx+(width*dmy))*3)]=255; // красный пиксель в полную яркость

  tex_data[1+((dmx+(width*dmy))*3)]=255; // зеленый пиксель в полную яркость

  tex_data[2+((dmx+(width*dmy))*3)]=255; // синий пиксель в полную яркость

}

 

Функция Reset делает довольно большую работу. Во-первых, здесь очищается текстура, потом задаются случайные цвета для каждой области просмотра, сбрасываются все стены лабиринта и, наконец, задается новая случайная начальная точка для генератора лабиринта.

 

В первой строке кода выполняется очистка. Переменная tex_data указывает на данные нашей текстуры. Нам необходимо очистить память размером с ширину (ширина нашей текстуры), умноженную на высоту (высоту текстуры) и еще умноженную на 3 (красный, зеленый, синий). При очистке памяти во всех байтах будет содержаться 0. Если все три цвета будут иметь значение 0, то наша текстура будет вся черная!

 

void Reset (void) // сбрасывает лабиринт, цвета, начальную точку и т.д.

{

  ZeroMemory(tex_data, width * height *3); // очистка текстуры нулями

 

Теперь зададим случайный цвет для каждой области просмотра. Для тех из вас, кто еще не знаком с этим, хочу сообщить, что случайное число на самом деле не случайно! Если вы создали программу, печатающую 10 случайных чисел, то, запуская, ее вы каждый раз получите одни и те же числа. Для задания более случайных чисел (по крайней мере, кажущимися более случайными) мы можем воспользоваться начальным случайным значением. И еще раз, если мы зададим такое начальное случайное значение как 1, то мы получим одни и те же цифры на выходе. Однако если мы в функции srand используем значение нашего счетчика времени (которое может быть любым числом), то в результате при каждом запуске программы мы получим разные значения.

 

У нас имеется 4 области просмотра, поэтому нам надо сделать цикл от 0 до 3. Зададим для каждого цвета (красного, зеленого, голубого) случайное значение из диапазона от 0 до 255. Я добавляю 128, чтобы получить яркие цвета. Так как минимальное значение 0 и максимальное 255, то значение 128 будет иметь соответствовать 50% яркости.

 

  srand(GetTickCount());            // пытаемся получить больше случайности

  for (int loop=0; loop<4; loop++)  // задаем 4 случайных цвета

  {

    r[loop]=rand()%128+128;         // задаем случайный красный (яркий)

    g[loop]=rand()%128+128;         // задаем случайный зеленый (яркий)

    b[loop]=rand()%128+128;         // задаем случайный синий (яркий)

  }

 

Далее мы задаем случайную начальную точку. Нам надо начать в комнате. Каждый второй пиксель текстуры – это комната. Чтобы быть уверенным, что мы начинаем в комнате, а не на стене, выберем случайное число от 0 до половины ширины текстуры и умножим на 2. При этом мы можем получить только 0, 2, 4, 6, 8 и т.д. Что означает, что мы каждый раз попадаем в случайную комнату и никогда не начнем со стены, значения которой составляют 1, 3, 5, 7, 9 и тд.

 

  mx=int(rand()%(width/2))*2;  // выбираем новое случайное положение по X

  my=int(rand()%(height/2))*2; // выбираем новое случайное положение по Y

}

 

Первая строчка в инициализации является очень важной. В ней выделяется память, достаточная для хранения нашей текстуры (width*height*3). Если не выделить память, то в лучшем случае произойдет крах системы.

 

// все GL и пользовательские инициализации находятся здесь

BOOL Initialize (GL_Window* window, Keys* keys)

{

  tex_data=new BYTE[width*height*3];// выделяем пространство для текстуры

 

  g_window  = window;                // параметры окна

  g_keys    = keys;                  // параметры клавиатуры

 

Сразу же после выделения памяти для нашей текстуры мы вызываем Reset(). Reset очистит текстуру, задаст цвета и выберет случайную начальную точку для нашего лабиринта.

 

После того как все было задано, нам надо создать нашу начальную текстуру. Первые два параметра текстуры  фиксируют ее диапазон к величине [0, 1]. Это предотвратит нежелательные эффекты при привязке единственного изображения к обьекту. Чтобы убедиться в том, что это важно, уберите 2 строчки этого кода. Без фиксации диапазона можно заметить тонкую линию наверху и на правой стороне текстуры. Эти линии появляются из-за того, что линейная фильтрация пытается смягчить всю текстуру, включая границы. Если пиксели нарисованы слишком близко к границе, то линия появится на противоположной стороне текстуры.

 

Мы будем использовать линейную фильтрацию, чтобы сделать изображение немного более сглаженым. Мы имеем полное право выбрать любой фильтр. Если он будет очень медленно работать, то смените его на GL_NEAREST.

 

Наконец мы построим RGB 2-мерную текстуру используя tex_data (альфа канал не будет использован).

 

  Reset(); // вызов Reset для построения нашей начальной текстуры и др.

  // начало инициализации параметров текстуры

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, tex_data);

и часто используется вместо вызова glMaterial, который сильно снижает общую скорость процесса. Мне пришло большое число сообщений по электронной почте, в которых спрашивается как сменить цвет обьекта… надеюсь вы получили полезную информацию! Для тех кто спрашивал почему текстуры в ваших проектах имеют такие ужасные цвета или имеют какой-то оттенок при вызове glColor( )... Проверьте, чтобы GL_COLOR_MATERIAL не был включен!

 

* Спасибо James Trotter за правильное обьяснение того, как GL_COLOR_MATERIAL работает. Я сказал, что это позволит вам задать цвета ваших текстур… На самом деле это позволит вам раскрасить ваши обьекты.

 

Наконец мы включаем наложение текстур.

 

  glClearColor (0.0f, 0.0f, 0.0f, 0.0f);// черный фон

  glClearDepth (1.0f);       // глубина буфера

 

  glDepthFunc (GL_LEQUAL);   // тип проверки глубины

  glEnable (GL_DEPTH_TEST);  // включение проверки глубины

 

  // включение цвета материала (возможность задания оттенков текстуры)

  glEnable(GL_COLOR_MATERIAL);

 

  glEnable(GL_TEXTURE_2D);     // включение наложения текстур

 

В следующем коде создается указатель на квадратичный обьект. Когда мы получим этот указатель, мы создаем нормали для сглаживания и координаты текстуры. При таком подходе освещение будет правильно работать, и текстура будет привязана к любому квадратичному обьекту автоматически!

 

   quadric=gluNewQuadric(); // создаем указатель на наш квадратичный обьект

   gluQuadricNormals(quadric, GLU_SMOOTH); // создаем сглаживающие нормали

   gluQuadricTexture(quadric, GL_TRUE);  // создаем координаты текстуры

 

Включаем источник освещения под номером 0, но ничего не произойдет до тех пор, пока мы не задействуем само освещение. Для тех кто еще не знает источник света 0 – это уже предопределенный свет, падающий прямо на экран. Это полезно в том случае, если вы чувствуете, что не можете самостоятельно правильно задать свет.

 

  glEnable(GL_LIGHT0);  // задание источника света 0 (свет GL по умолчанию)

  return TRUE;          // возвращаем TRUE (удачная инициализация)

}

 

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

 

void Deinitialize (void)  // деинициализация

{

  delete [] tex_data;  // Убираем наши данные текстуры (Освобождаем память)

}

 

В функции Update( ) выполяется создание лабиринта, а так же слежение за нажатием клавиатуры, вращением и т.д.

 

Нам надо задать переменную dir. Эту переменную мы будем использовать для случайного перемещения вверх, вправо, вниз или влево.

 

Здесь мы наблюдаем за нажатием пробела. Если он был нажат, и он не был удержан нажатым, мы восстановим лабиринт. Если клавиша была отпущена, то зададим sp значением FALSE для того, чтобы наша программа знала, что нажатие больше не происходит.

 

void Update (float milliseconds) // выполним обновление

{

  int  dir;                    // содержит текущее направление

  if (g_keys->keyDown [VK_ESCAPE])   // была нажата ESC?

    TerminateApplication (g_window); // прервать программу

 

  if (g_keys->keyDown [VK_F1])       // была нажата F1?

    ToggleFullscreen (g_window);     // переключить на полный зкран

  if (g_keys->keyDown [' '] && !sp)  // проверка нажатия пробела  {

    sp=TRUE;                  // если да, установим sp в TRUE (пробел нажат)

    Reset();                  // вызываем Reset и начинаем новый лабиринт

  }

 

  if (!g_keys->keyDown [' ']) // проверяем если пробел был отпущен

    sp=FALSE;                 // устанавливаем sp в FALSE (пробел отпущен)

 

Переменные xrot, yrot and zrot увеличиваются на значение прошедших миллисекунд, умноженных на некоторую малую вещественную переменную. Это позволит нам вращать обьект по осям х, у и z.  Каждая переменная будет увеличена на различную величину, чтобы сделать вращение немного более приятным.

 

  xrot+=(float)(milliseconds)*0.02f; // увеличить вращение по оси X

  yrot+=(float)(milliseconds)*0.03f; // увеличить вращение по оси Y

  zrot+=(float)(milliseconds)*0.015f; // увеличить вращение по оси Z

 

Следующий код проверяет, закончено или нет рисование лабиринта. Мы начинаем с задания done значения TRUE. Затем мы в цикле проходим через каждую комнату с целью определить, если хоть одна стена, которая нуждается в обработке. Если какая то комната еще не посещалась, мы зададим выполнение в FALSE.

 

Если tex_data[((x+(width*y))*3)] равно нулю, это означает, что комната еще не посещалась и пиксели еще не были нарисованы. Если же был отрисован какой-то пиксель, то его значение было бы 255. Мы проверяем только красный пиксель, т.к. мы знаем, что значение красного могло бы быть или 0 (пустой) или 255 (обработанный).

 

  done=TRUE;             // зададим выполнение в True

  for (int x=0; x<width; x+=2) // цикл через все комнаты

  {

    for (int y=0; y<height; y+=2) // по осям X и Y

    {

      // если пиксель текущей текстуры (комнаты) является пустым

      if (tex_data[((x+(width*y))*3)]==0)

        done=FALSE;     // надо задать False (все еще не выполнено)

    }

  }

 

Если после проверки всех комнат done имеет значение TRUE, то мы знаем, что лабиринт завершен. SetWindowsText сменит название окна на название “Лабиринт завершен”. Затем подождем 5000 миллисекунд, чтобы пользователь смог прочитать сообщение (если пользователь задействовал полный экран, то он заметит, что анимация остановилась). После 5000 миллисекунд мы изменим название окна в “Построение лабиринта!” и на самом деле восстановим лабиринт (начиная весь процесс сначала).

 

  if (done) // если выполнение завершено, то все комнаты были посещены

  {

    // выдаем сообщение вверху окна, сделаем паузу

    // и начнем построение следующего лабиринта!

    SetWindowText(g_window->hWnd,"Lesson 42: Multiple Viewports... 2003 NeHe Productions... Maze Complete!");

    Sleep(5000);

    SetWindowText(g_window->hWnd,"Lesson 42: Multiple Viewports... 2003 NeHe Productions... Building Maze!");

    Reset();

  }

 

Следующий код может показаться странным, но на самом деле он не так сложен для понимания. Здесь мы проверяем посещалась ли комната справа от текущей, а так же если наше текущее положение близко к правой границе лабиринта (вправо больше нет комнат). Проверяем, если мы посетили комнату слева или мы близко к левой границе лабиринта (слева больше нет комнат). Проверяем, если мы посетили комнату снизу или мы близко к нижней границе лабиринта (снизу больше нет комнат) и, наконец, мы проверяем посещали ли комнату сверху или мы близко к верхней границе (сверху больше нет комнат).

 

Если значение пикселя комнаты равно 255, то это означает, что комнату посетили (т.к. значение было изменено в UpdateTex). Если mx (текущее положение по х) меньше чем 2, то мы знаем, что находимся у левого края экрана и не можем далее идти влево.

 

Если мы на границе или близко к ней, задаем случайные значения mx и my. После этого проверяем, если этот пиксель мы уже посещали. Если нет, то задаем новые случайные значения mx, my до тех пор, пока не найдем новую ячейку, которую уже посетили. Мы хотим чтобы новые пути ответвлялись от старых, поэтому нам надо искать до тех пор, пока мы не найдем старый путь, чтобы начать с него.

 

Чтобы код был минимальным, я не проверяю на “mx-2 меньше 0”. Если желаете 100% проверки ошибок, вы можете изменить этот фрагмент кода для проверки памяти, которая не принадлежит текущей текстуре.

 

  // проверяем, что мы не застряли (некуда двигаться)

  if (((mx>(width-4) || tex_data[(((mx+2)+(width*my))*3)]==255)) &&

       ((mx<2 || tex_data[(((mx-2)+(width*my))*3)]==255)) &&

       ((my>(height-4) || tex_data[((mx+(width*(my+2)))*3)]==255)) &&

       ((my<2 || tex_data[((mx+(width*(my-2)))*3)]==255)))

  {

    do                    // если мы застряли

    {

      mx=int(rand()%(width/2))*2; // задаем новое случайное значение X

      my=int(rand()%(height/2))*2;      // задаем новое случайное значение У

    }

    // продолжаем случайный выбор до тех пор пока не найдем

    while (tex_data[((mx+(width*my))*3)]==0);

  }  // нашли точку старта

 

Первая строчка ниже задает случайное значение для dir в диапазоне 0.. 3. Эти значения будут использованы для рисования лабиринта справа, вверху, слева, внизу.

 

После задания случайных направлений, проверяем, если значение dir равно 0 (движение вправо). Если это так и мы не на самой правой границе лабиринта, то проверяем комнату справа от текущей. Если ее не посещали, убираем стену между комнатами в UpdateTex(mx+1,my) и перемещаемся в новую комнату, увеличивая mx на 2.

 

  dir=int(rand()%4);      // выбираем случайное направление

  // если направление 0 (вправо) и мы не на самой правой границе

  if ((dir==0) && (mx<=(width-4)))

  {

    // и если комнату справа еще не посещали

    if (tex_data[(((mx+2)+(width*my))*3)]==0)

    {

      // изменяем текстуру, показывая путь между комнатами

      UpdateTex(mx+1,my);

      mx+=2;             // движемся вправо (комната вправо)

    }

  }

 

Если значение dir равно 1 (вниз) и мы находимся не в самом низу, то проверяем, если эту комнату посещали. Если ее еще не посещали, убираем стену между комнатами (текущей комнаты и комнаты ниже) и движемся в новую комнату, увеличивая my на 2.

 

  // если направление 1 (вниз) и мы не в самом низу

  if ((dir==1) && (my<=(height-4)))

  {

    // и если не посещали комнату внизу

    if (tex_data[((mx+(width*(my+2)))*3)]==0)

    {

      // Обработать текстуру для показа пути, вырезанного между комнатами

      UpdateTex(mx,my+1);

      my+=2;                  // перейти вниз (комнату снизу)

    }

  }

 

Если  значение dir составляет 2 (слева) и мы не на самой левой границе, то проверяем, если мы не посещали левую комнату. Если мы там не были, то убираем стену между этими комнатами (текущей и комнатой слева) и перемещаемся в следующую комнату, уменьшая mx на 2.

 

  // если направление равно 2 (влево) и мы не на самом левом положении

  if ((dir==2) && (mx>=2))

  {

    // и если комната слева не посетили

    if (tex_data[(((mx-2)+(width*my))*3)]==0)

    {

      // обработать текстуру для показа пути между двумя комнатами

      UpdateTex(mx-1,my);

      mx-=2;             // двигаться влево (в комнату слева)

    }

  }

 

Если значение dir составляет 3 (вверх) и мы не на самом верху, то проверяем, если мы посещали комнату вверху. Если еще не посещали, то убираем стенку между комнатами (текущей и комнатой сверху) и перемещаемся в новую комнату, уменьшая my на 2.

 

  // если направление 3 (вверх) и мы не на самом верху

  if ((dir==3) && (my>=2))

  {

    // и если не посещали комнату вверху

    if (tex_data[((mx+(width*(my-2)))*3)]==0)

    {

      UpdateTex(mx,my-1); // обработать текстуру для показа нового пути

      my-=2;       // движемся вверх (комната сверху)

    }

  }

 

После перемещения в новую комнату мы маркируем ее как посещенную. Мы делаем это в UpdateTex( ) с текущими значениями положения mx, my.

 

  UpdateTex(mx,my); // обработка текущей комнаты

}

 

Мы начнем следующий раздел кода с кое-чего нового… Нам надо знать какой размер текущего окна, чтобы правильно задать размер просмотровой зоны. Чтобы получить размер текущего окна нам надо получить левое положение окна, правое положение окна, верхнее положение окна и нижнее значение. Имея эти величины мы можем вычислить ширину, вычитая левое значение из правого. Так же можно получить высоту вычитая верхнее положение окна из нижнего.

 

Значения левое, правое, верхнее, нижнее можно получить используя RECT. Структура RECT содержит координаты прямоугольника. Сказать точнее левую, правую, верхнюю и нижнюю координаты.

 

Для получения координат нашего экрана мы используем GetClientRect( ). Первый параметр, который мы передаем в эту команду, будет указатель нашего окна. Второй параметр будет структурой, в которой содержится возвращаемая информация (rect).

 

void Draw (void)   // наша функция рисования

{

  RECT  rect;      // содержит координаты прямоугольника

  GetClientRect(g_window->hWnd, &rect); // получаем размер окна

  int window_width=rect.right-rect.left; // вычисляем ширину (справа - слева)

  int window_height=rect.bottom-rect.top; // вычисляем высоту (низ-верх)

 

Нам надо пересоздать текстуру в каждом кадре и нам надо выполнить это перед тем, как мы нарисуем сцены с текстурой. Наибыстрейшим способом обновить содержимое текстуры является функция glTexSubImage2D( ). Функция glTexSubImage2D меняет всю или часть текстуры хранящуюся в экранной памяти. В коде, приведенном ниже, мы говорим, что используем 2-мерную текстуру. Номер уровня деталей задается 0, нам не надо смещения относительно x (0) или y (0). Мы хотим использовать полную ширину и высоту текстуры. Данные имеют формат GL_RGB, и их типом является GL_UNSIGNED_BYTE. tex_data - это данные, которые мы хотим передать.

 

Это очень быстрый путь обновить данные текстуры без перестройки самой текстуры. Важно заметить, что эта команда не будет СОЗДАВАТЬ текстуру. Чтобы изменить текстуру этой командой вам надо создать эту текстуру.

 

  // изменить текстуру... это ключевое место для скорости программы...

  // намного быстрее, чем пересоздавать текстуру каждый раз

 glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGB,

    GL_UNSIGNED_BYTE, tex_data);

 

Эта строка кода очень важная. В ней очищается весь экран. Поскольку нам надо очистить экран ПЕРЕД тем как все 4 просмотровые области нарисуются, нам надо его очистить перед основным циклом, в котором все 4 просмотровые области нарисуются вновь! Это ОЧЕНЬ важно, что вы очищаете экран всего один раз и потом очищаете глубину буфера перед отрисовкой каждой области просмотра.

 

  glClear (GL_COLOR_BUFFER_BIT);  // очистка экрана

 

Теперь об основном цикле рисования. Нам надо нарисовать 4 различные области просмотра, поэтому задаем цикл от 0 до 3.

 

С самого начала мы зададим цвет текущей области просмотра с помощью glColor3ub(r,g,b). Этот шаг может показаться новым для некоторых из вас. Эта функция аналогична glColor3f(r,g,b), но использует значение беззнаковых байтов вместо значений с плавающей запятой. Вы должны помнить, как я ранее уже сказал, что гораздо легче задать случайное значение от 0 до 255 для цвета. Теперь же, когда мы имеем такие большие значения для каждого цвета, нам нужна именно эта команда, чтобы правильно задать цвета.

 

Вызов glColor3f(0.5f,0.5f,0.5f) задает цвет 50% яркости для красного, зеленого и синего. Вызов glColor3ub(127,127,127) также задает цвет 50% яркости.

 

Если значение цикла равно 0, то мы выбираем r[0],g[0],b[0]. Если значение цикла равно 1 , то мы выбираем цвета из r[1],g[1],b[1]. Таким образом, каждая сцена будет иметь свой случайный цвет.

 

  for (int loop=0; loop<4; loop++)  // цикл отрисовки всех 4 просмотров

  {

    // задание цвета для текущего просмотра

    glColor3ub(r[loop],g[loop],b[loop]);

 

Перед тем как мы сможем, что-либо нарисовать надо задать текущую область просмотра. Если значение цикла равно 0, то мы рисуем в первой области. Нам надо поместить эту область в левой части экрана (0), в верхней половине (window_height/2). Ширина области будет равна половине ширины основного окна (window_width/2), а высота – половине высоты основного окна  (window_height/2).

 

Если основное окно имеет размер 1024x768, то наша область просмотра будет начинаться в 0,384 и иметь размер по ширине 512 и по высоте 384.

 

Область просмотра должна выглять так:

 

 

После задания области просмотра выбираем матрицу проекции, сбрасываем ее и затем устанавливаем наш 2-мерный ортонормальный просмотр. Мы хотим, чтобы наш ортогональный просмотр заполнил всю область просмотра. Поэтому мы задаем его левое значение 0, а правое величиной window_width/2 (та же величина, что и ширина просмотровой области). Нижнее значение зададим window_height/2, а верхнее 0. При этом получается та же высота, что и области.

 

Левая верхняя точка нашего ортогонального просмотра будет 0,0. Правая нижняя точка будет иметь значения window_width/2, window_height/2.

 

    if (loop==0)   // если мы рисуем первую сцену

    {

      // зададим область просмотра вверху, слева.

      // Он займет половину высоты и ширины экрана

      glViewport (0, window_height/2, window_width/2, window_height/2);

      glMatrixMode (GL_PROJECTION);  // выбираем матрицу проекции

      glLoadIdentity ();              // сбрасываем матрицу проекции

      // задаем ортогональный режим просмотра,

      // растягивая на 1/4 экрана (размер области просмотра)

      gluOrtho2D(0, window_width/2, window_height/2, 0);

    }

 

Если значение цикла равно 1, то мы рисуем вторую область просмотра. Она будет справа, сверху основного окна. Ширина и высота будут иметь те же значения что и первая область просмотра. Единственный параметр, который изменяется, будет первый параметр в функции glViewport( ), он будет иметь значение window_height/2.

 

Вторая область просмотра должна выглять так:

 

 

 

Вновь мы выбираем матрицу проекций и сбрасываем ее, но в этот раз зададим перспективный просмотр с полем обзора 45 градусов, со значением ближней плоскости 0.1 и значением дальней плоскости 500.0f.

 

    if (loop==1)   // если рисуем вторую область просмотра

    {

      // задаем область вверху справа.

      // Она займет половину экрана по ширине и высоте

      glViewport (window_width/2, window_height/2,

                  window_width/2, window_height/2);

      glMatrixMode (GL_PROJECTION);  // выбираем матрицу проекций

      glLoadIdentity ();              // сбрасываем матрицу проекций

      // задаем перспективный просмотр с растяжкой на 1/4 экрана

      // (размер области просмотра)

      gluPerspective( 45.0, (GLfloat)(width)/(GLfloat)(height),

        0.1f, 500.0 );

    }

 

Если значение цикла равно 2, то мы рисуем третью область просмотра. Она будет справа, внизу основного окна. Ширина и высота будут иметь те же значения что и первая и вторая области просмотра. Единственный параметр, который изменяется от второй области, будет второй параметр в функции glViewport( ), который принимает значение 0. Это говорит о том, что мы хотим, чтобы область просмотра началась внизу справа основного окна.

 

Третья область просмотра должна выглять так:

 

 

 

Зададим перспективный просмотр точно так же, как мы сделали для второй области просмотра.

 

    if (loop==2)   // если рисуем третью сцену

    {

      // устанавливаем область просмотра в нижний правый угол.

      // Она займет половину ширины и высоты окна

      glViewport (window_width/2, 0, window_width/2, window_height/2);

      glMatrixMode (GL_PROJECTION); // выбираем матрицу проекции

      glLoadIdentity ();              // сбрасываем матрицу проекции

      // задаем перспективный просмотр с растяжением на 1/4 экрана

      // (размер области просмотра)

      gluPerspective( 45.0, (GLfloat)(width)/(GLfloat)(height),

         0.1f, 500.0 );

    }

 

Если значение цикла равно 3, то мы рисуем последнюю область просмотра (под номером 4). Она будет слева, внизу основного окна. Ширина и высота будут иметь те же значения что в первой, второй и третьей областях просмотра. Единственный параметр, который изменяется от третьей области, будет первый параметр в функции glViewport( ), который принимает значение 0. Это говорит о том, что мы хотим, чтобы область просмотра началась внизу слева основного окна.

 

Четвертая область просмотра должна выглять так:

 

 

Задаем перспективный просмотр точно так же как во второй области.

 

    if (loop==3)  // если рисуем четвертую сцену

    {

      // задаем область просмотра внизу слева.

      // Она займет половину ширины и высоты экрана

      glViewport (0, 0, window_width/2, window_height/2);

      glMatrixMode (GL_PROJECTION);  // задаем матрицу проекции

      glLoadIdentity ();             // сбрасываем матрицу проекции

      // задаем перспективный просмотр с растяжением на 1/4 экрана

      // (размер области просмотра)

      gluPerspective( 45.0, (GLfloat)(width)/(GLfloat)(height),

        0.1f, 500.0 );

    }

 

В следующем коде мы выбираем матрицу просмотра модели, сбрасываем ее, затем очищаем буфер глубины. Мы очищаем буфер глубины для всех нарисованных областей просмотра. Заметьте, что мы не очищаем цвет экрана. Только буфер глубины! Если его не очистить, то вы можете заметить, что фрагменты обьектов исчезают и т.п. Определенно некрасиво!

 

    glMatrixMode (GL_MODELVIEW);      // выбор матрицы просмотра модели

    glLoadIdentity ();                // сброс матрицы просмотра модели

    glClear (GL_DEPTH_BUFFER_BIT);    // очистка буфера глубины

 

Первым изображением, которое мы нарисуем, будет плоский 2-мерный квадрат с текстурой. Квадрат будет нарисован в ортогональном режиме и займет всю площадь области просмотра. Из-за того, что мы используем прямоугольный режим просмотра, третье измерение отсутствует, поэтому нет необходимости проектировать на ось z.

 

Помните, что левая верхняя координата первой области просмотра расположена в 0,0 и правая нижняя точка в window_width/2, window_height/2. Это означает, что правая верхняя координата нашего квадрата будет в window_width/2, 0. Левая верхняя точка квадрата в 0,0, нижняя левая в  0, window_height/2 и нижняя правая в window_width/2, window_height/2. Замечу еще, что в ортогональном режиме мы на самом деле можем работать с пикселями вместо реальных значений (в зависимости от того, как мы задали верх области просмотра).

 

    // это первое изображение? (начальная текстура, прямоугольная проекция)

    if (loop==0)

    {

      glBegin(GL_QUADS); // начали рисовать один квадрат

        // заполним всю 1/4 область единственным квадратом с текстурой.

        glTexCoord2f(1.0f, 0.0f); glVertex2i(window_width/2, 0);

        glTexCoord2f(0.0f, 0.0f); glVertex2i(0, 0);

        glTexCoord2f(0.0f, 1.0f); glVertex2i(0, window_height/2);

        glTexCoord2f(1.0f, 1.0f); glVertex2i(window_width/2, indow_height/2);

      glEnd();                // закончили рисование квадрата с текстурой

    }

 

Второе изображение будет плавной сферой с подсветкой. Вторая область просмотра имеет персперктивный просмотр, поэтому сначала нам надо передвинуться внутрь экрана на 14 единиц. Затем мы вращаем наш обьект по х, у, z осям.

 

Сделаем возможной подсветку, нарисуем сферу и затем выключим подсветку. Сфера имеет радиус 4 единицы с 32 срезами по вертикали и 32 срезами по горизонтали. Если вам захочется изменить что-нибудь, попробуйте изменить число разрезов на меньшее значение. Уменьшая число разрезов, вы заметите, что гладкость сферы пропадает.

 

Координаты текстуры будут автоматически сгенерированы!

 

    // мы рисуем второе изображение?

    // (3-мерная сфера с текстурой... Перспективный просмотр)

    if (loop==1)

    {

      glTranslatef(0.0f,0.0f,-14.0f); // двигаемся на 14 единиц внутрь экрана

      glRotatef(xrot,1.0f,0.0f,0.0f); // вращаем на величину xrot по оси X

      glRotatef(yrot,0.0f,1.0f,0.0f); // вращаем на величину yrot по оси Y

      glRotatef(zrot,0.0f,0.0f,1.0f); // вращаем на величину zrot по оси Z

      glEnable(GL_LIGHTING);          // разрешеаем осещение

      gluSphere(quadric,4.0f,32,32);  // рисуем сферу

      glDisable(GL_LIGHTING);         // запрещаем освещение

    }

 

Третье изображение будет таким же, как первое, но будет нарисовано в перспективе. Оно будет наклонено на определенный угол, и будет вращаться (на самом деле!).

 

Мы передвинемся на 2 единицы внутрь экрана и наклоним квадрат на 45 градусов. При этом верх квадрата будет расположен дальше от нас, а низ – ближе к нам.

 

Затем мы вращаем по оси z, чтобы заставить квадрат крутиться и, наконец, рисуем квадрат. В этот раз нам надо задать координаты текстуры вручную.

 

    // мы рисуем третье изображение?  (текстура с наклоном... в перспективе)

    if (loop==2)

    {

      glTranslatef(0.0f,0.0f,-2.0f);  // движемся внутрь экрана на 2 единицы

      glRotatef(-45.0f,1.0f,0.0f,0.0f);// наклоняем квадрат назад на 45 град

      // вращаем на величину zrot/1.5 по оси Z

      glRotatef(zrot/1.5f,0.0f,0.0f,1.0f);

 

      glBegin(GL_QUADS);              // начало рисования квадрата

        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, 0.0f);

        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, 0.0f);

        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 0.0f);

        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 0.0f);

      glEnd();                // конец рисования квадрата с текстурой

    }

 

При рисовании четвертого изображения мы передвигаемся на 7 единиц внутрь экрана. Затем вращаем обьект по осям х, у, z.

 

Включим освещение, чтобы получить красивое сглаживание и затем перемещаемся на –2 единицы по оси z. Причина, по которой мы это делаем, заключается в том, что наш обьект будет вращаться вокруг своей центральной точки, а не вокруг одного из своих краев. Ширина цилиндра на одном краю составляет 1.5 единицы и 1.5 единицы на другом краю, длина составляет 4  единицы, цилиндр будет иметь 32 среза по окружности и 16 срезов по длине.

 

Для вращения обьекта вокруг его центра нам надо переместиться на половину длины. Половина от 4 составляет 2!

 

После перемещения, вращения и еще одного перемещения мы нарисуем цилиндр и выключим освещение.

 

    // мы рисуем четвертое изображение?

    // (3-мерный цилиндр с текстурой... в перспективе)

    if (loop==3)

    {

      glTranslatef(0.0f,0.0f,-7.0f);  // движемся на 7 единиц внутрь экрана

      glRotatef(-xrot/2,1.0f,0.0f,0.0f);// вращаем на -xrot/2 по оси X

      glRotatef(-yrot/2,0.0f,1.0f,0.0f); // вращаем на -уrot/2 по оси Y

      glRotatef(-zrot/2,0.0f,0.0f,1.0f); // вращаем на -zrot/2 по оси Z

 

      glEnable(GL_LIGHTING);              // включаем освещение

      // перемещаемся на -2 по Z (чтобы вращать цилиндр вокруг его центра,

      // а не края)

      glTranslatef(0.0f,0.0f,-2.0f);

      gluCylinder(quadric,1.5f,1.5f,4.0f,32,16); // рисуем цилиндр

      glDisable(GL_LIGHTING);              // выключаем освещение

    }

  }

 

И, наконец, выводим отрисованный поток.

 

  glFlush ();    // выводим отрисованный поток GL

}

 

Надеюсь, этот урок ответил на вопросы по созданию множественных областей просмотра. Код совсем не сложный для понимания. Он почти, что идентичен основному стандартному коду. Основные отличия заключается в том, что задание области просмотра было выполнено в основном цикле рисования, экран очищался один раз перед рисованием областей просмотра и буфер глубины чистился отдельно.

 

Вы можете использовать код этого урока для разнообразных изображений, каждое из которых будет находиться в своей области просмотра. Или вы можете использовать этот код для просмотра одного обьекта в нескольких областях. Можете делать с кодом все что хотите.

 

Надеюсь вам, друзья, понравился урок. Если найдете, какие либо ошибки в коде, или почувствуете, что можно сделать этот урок еще лучше, то дайте мне знать.

 

© Jeff Molofee (NeHe)

PMG  20 августа 2006 (c)  Геннадий Хохорин