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

Loading Textures From A Resource File & Texturing Triangles

 

Добро пожаловать в 38-ой урок NeHe Productions. Со времени публикации моего последнего урока прошло довольно много времени, поэтому мой стиль написания изменился. На это накладывается еще то, что я провел почти 24 часа над кодом.

 

Теперь вы знаете, как накладывать текстуру на квадрат, а также как загружать растровые изображения в различных форматах: tga и др.. Так как же все-таки делать наложение текстуры на треугольник? И что делать, если вы захотите поместить текстуру в .ехе файл?

 

Эти два вопроса мне задают каждый день, и ответ на них вы скоро получите. После того как вы увидите, как это легко делать, возможно, вы удивитесь, почему же вам самим до сих пор не пришел ответ :^).

 

Вместо того чтобы обьяснять все детали в подробностях, я приведу несколько снимков с экрана, и вы сразу поймете, о чем идет речь. Я использую последний вариант базового кода. Вы можете загрузить этот код с основной страницы из раздела “NeHeGL 1 Basecode” или можно взять весь код в конце этой статьи.

 

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

 

Важно, что этот урок написан для Visual C++ 6.0. Если вы используете какой-то другой компилятор, то раздел урока по работе с ресурсным файлом не будет иметь смысла (особенно снимки с экрана).

 

·       В настоящее время вы можете пользовать только 24-битовые BMP изображения. Есть много приложений для загрузки 8-битовых ВМР. Я хотел бы услышать от кого-нибудь о маленьком оптимизированном ВМР загрузчике. Тот код, который сейчас есть у меня для 8-бит и 24-бит ВМР выглядит ужасно. Что-нибудь использующее команду LoadImage было бы прекрасно.

 

 

 

Итак, откройте проект и нажмите INSERT на основном меню. После того как INSERT меню откроется, нажмите RESOURCE.

 

 

 

Теперь надо выбрать какой тип ресурса вы хотите импортировать. Выберите BITMAP и нажмите кнопку IMPORT.

 

 

 

Откроется диалог выбора файла. Найдите директорию DATA и выберите все три изображения (при выборе изображений удерживайте кнопку CTRL). После того как все три будут выбраны, нажмите кнопку IMPORT. Если вы не видите .bmp файлы, то проверьте что FILES OF TYPE внизу диалога показывает ALL FILE(*.*).

 

 

 

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

 

 

 

После того как все 3 изображения импортированы, появится соответствующий список. Каждое изображение будет иметь свой ID (идентификатор). Каждый ID состоит из IDB_BITMAP и цифры от 1 до 3. Если вам не хочется, то можно не менять эти ID и перейти сразу к коду. Нам повезло, что мы не такие ленивые!

 

 

 

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

 

 

 

 

После этого нажмите FILE на основном меню и потом SAVE ALL. Из-за того, что вы только что создали новый файл ресурсов, система спросит название файла. Можно сохранить с первоначальным названием или переименовать его в lesson38.rc. После этого нажмите SAVE.

 

До этого момента добралось большинство народа. Теперь у вас есть файл ресурсов. Он содержит изображения, и он сохранен на диске. Чтобы использовать изображения надо сделать еще несколько шагов.

 

 

Следующее что надо сделать – это добавить файл ресурсов к своему проекту. На основном меню нажмите PROJECT, ADD TO PROJECT и потом FILES.

 

 

 

Выберите resource.h и файл ресурсов (lesson38.rc). Держите CTRL при выборе нескольких файлов или добавляйте их по одному.

 

 

 

Осталось удостовериться, что файл ресурсов (lesson38.rc) попал в раздел RESOURCE FILES. Как вы видите на верхней картинке, он сейчас находится в SOURCE FILES. Нажмите левой кнопкой мыши на этот файл и переместите в раздел RESOURCE FILES.

 

После этого нажмите FILE на основном меню и потом SAVE ALL. Самая тяжелая работа уже сделана! Было столько много картинок :^).

 

Теперь займемся кодом! Наиболее важная строчка в коде ниже это #include “resource.h”. Без нее при компиляции вы получите кучу ошибок undeclared identifier. В файле resource.h объявляются объекты внутри файла ресурсов. Поэтому если вы хотите использовать данные из IDB_BUTTERFLY1 не забудьте включить этот header!

 

 

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

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

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

#include <gl\glaux.h>                 // Заголовочный файл библиотеки GLaux

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

#include "resource.h"                 // Заголовочный файл для ресурсов (*ВАЖНО*)

 

#pragma comment( lib, "opengl32.lib" ) // Искать OpenGL32.lib при линковке

#pragma comment( lib, "glu32.lib" )   // Искать GLu32.lib при линковке

#pragma comment( lib, "glaux.lib" )   // Искать GLaux.lib при линковке

 

#ifndef CDS_FULLSCREEN                // CDS_FULLSCREEN не определяется некоторыми

#define CDS_FULLSCREEN 4              // компиляторами. Определяем эту константу

#endif                                // Таким образом мы можем избежать ошибок

 

GL_Window*  g_window;

Keys*    g_keys;

 

 

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

 

Будет использована структура для информации о 50 различных объектах, которые мы будем двигать по экрану.

 

Переменная tex содержит информацию о текстуре объекта. x – это положение объекта по оси X, y- это Y положение объекта, z – положение объекта по оси Z, yi будет случайным числом, описывающим скорость падения объекта, spinz используется для вращения объекта по оси z во время падения,  spinzi это еще одно случайное число для описания скорости вращения объекта, flap используется для описания крыльев объекта (подробнее ниже), fi – это случайное число описывающее направление хлопка крыльев.

 

Далее в obj[] мы создаем 50 экземпляров объектов с этой структурой.

 

// Пользовательские переменные

GLuint texture[3];            // Место для хранения 3 текстур

 

struct object                 // Структура объекта

{

  int   tex;                  // Текстура

  float x;                    // X позиция

  float y;                    // Y позиция

  float z;                    // Z позиция

  float yi;                   // Y скорость падения

  float spinz;                // Z вращение

  float spinzi;               // Z скорость вращения

  float flap;                 // Хлопающие треугольники :)

  float fi;                   // Направление хлопанья

};

 

object obj[50];               // Создается 50 объектов

 

Часть кода ниже задает случайные начальные значения для любого обьекта с номером от 0 до 49 (для любого из зарезервированных 50).

 

Начнем со случайной текстуры от 0 до 2. Это приведет к случайному выбору расцветки бабочки.

 

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

 

Координата z получит также случайное значение из диапазона –10 .. –40, spinzi из диапазона –1.0… 1.0, flap получит значение 0.0 (центральное положение крыльев).

 

Наконец, скорость махания (fi) и скорость падения (yi) также получат случайные значения.

 

void SetObject(int loop)              // Инициализация объекта (случайная)

{

  obj[loop].tex=rand()%3;             // Текстура

  obj[loop].x=rand()%34-17.0f;        // x от -17.0f до 17.0f

  obj[loop].y=18.0f;                  // Y равно 18 (чуть выше экрана)

  obj[loop].z=-((rand()%30000/1000.0f)+10.0f);// z от -10.0f до -40.0f

  obj[loop].spinzi=(rand()%10000)/5000.0f-1.0f;// spinzi от -1.0f до 1.0f

  obj[loop].flap=0.0f;                // вначале хлопанье равно 0.0f;

  obj[loop].fi=0.05f+(rand()%100)/1000.0f;    // fi от 0.05f до 0.15f

  obj[loop].yi=0.001f+(rand()%1000)/10000.0f; // yi от 0.001f до 0.101f

}

 

Теперь самая интересная часть! Загрузка изображения из файла ресурсов и превращение его в текстуру.

 

hBMP – это указатель на наш файл изображения. Он говорит о том, откуда взять данные. BMP - это структура изображения, которую мы можем заполнить данными из файла ресурсов.

 

В третьей строчке мы говорим программе, какие ID будут использованы. Мы хотим загрузить IDB_BUTTERFLY1, IDB_BUTTERFLY2 и IDB_BUTTERFLY3 ID. Если надо добавить еще изображения, добавь их в файл ресурсов и добавь ID в TEXTURE[]:

 

void LoadGLTextures()             // Создание текстур из изображений, которые в ресурсе

{

  HBITMAP hBMP;                   // Указатель на изображение

  BITMAP  BMP;                    // структура изображения

 

  // Три ID для изображений, которые мы хотим загрузить из файла ресурсов

  byte  Texture[]={ IDB_BUTTERFLY1, IDB_BUTTERFLY2, IDB_BUTTERFLY3 };

 

В следующей строке используется sizeof(Texture) для определения необходимого количества текстур. У нас имеются 3 ID в Texture[] поэтому это значение равно 3. Функция sizeof(Texture) также будет использована в основном цикле.

 

glGenTextures(sizeof(Texture), &texture[0]); // создаем 3 текстуры (sizeof(Texture)=3 ID's)

for (int loop=0; loop<sizeof(Texture); loop++) // цикл по всем ID (изображений)

  {

 

Функция LoadImage принимает следующие параметры: GetModuleHandle(NULL) – указатель на объект. MAKEINTRESOURCE(Texture[loop]) – преобразует целое значение (Texture[loop]) в значение ресурса (в изображение для загрузки). IMAGE_BITMAP – говорит нашей программе о том, что загружаемый ресурс является изображением.

 

Следующие два параметра (0,0) задают желаемую высоту и ширину изображения в пикселях. Мы будем использовать размер по умолчанию, поэтому оба равны 0.

 

Последний параметр (LR_CREATEDIBSECTION) возвращает DIB часть изображения, которая является тем же изображением, но без цветовой информации. Именно то, что нам надо.

 

hBMP указывает на данные изображения, загруженного с помощью LoadImage().

 

hBMP=(HBITMAP)LoadImage(GetModuleHandle(NULL),MAKEINTRESOURCE(Texture[loop]), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);

 

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

 

Если данные существуют, то мы используем GetObject() для извлечения всех данных (sizeof(BMP)) из hBMP и сохраняем их в нашем BMP (структура изображения).

 

Функция glPixelStorei говорит OpenGL о том, что данные сохранены с выравниванием по размеру двойного слова (4 байта на каждый пиксель).

 

Затем мы связываемся с нашей текстурой, устанавливаем фильтр на GL_LINEAR_MIPMAP_LINEAR (плавное сглаживание) и генерируем нашу текстуру.

 

Заметьте, что мы используем BMP.bmWidth и BMP.bmHeight для получения высоты и ширины изображения. Также нам необходимо поменять местами Красный и Синий цвета с помощью GL_BGR_EXT. Данные ресурса будут извлечены из BMP.bmBits.

 

На последнем шаге мы удаляем объект изображения, чтобы освободить все системные ресурсы, связанные с объектом.

 

if (hBMP)             // существует ли изображение?

    {                 // если да …

      GetObject(hBMP,sizeof(BMP), &BMP);    // получить объект

                      // hBMP: указатель на графический объект

                      // sizeof(BMP): размер буфера для информации по объекту
                      // BMP - Буфер информации по объекту

      // режим сохранения пикселей (равнение по двойному слову / 4 Bytes)

      glPixelStorei(GL_UNPACK_ALIGNMENT,4);

      glBindTexture(GL_TEXTURE_2D, texture[loop]);  // связываемся с нашей текстурой

      // линейная фильтрация

      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

      // линейная фильтрация Mipmap

      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);

 

      // генерация Mipmapped текстуры (3 байта, ширина, высота и данные из BMP)

      gluBuild2DMipmaps(GL_TEXTURE_2D, 3, BMP.bmWidth, BMP.bmHeight,

                        GL_BGR_EXT, GL_UNSIGNED_BYTE, BMP.bmBits);

      DeleteObject(hBMP);              // удаление объекта изображения

    }

  }

}

 

В самом деле, нет ничего необычного в коде инициализации. Мы добавим LoadGLTextures()  для вызова кода, приведенного выше. Цвет пустого экрана – черный. Проверка на глубину выключена (самый легкий способ для смешивания). Мы включим отрисовку текстуры, затем зададим параметры и включим смешение.

 

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

BOOL Initialize (GL_Window* window, Keys* keys)

{

  g_window  = window;

  g_keys    = keys;

 

  // Start Of User Initialization

  LoadGLTextures();                  // Загрузка текстуры из нашего файла ресурсов 

  glClearColor (0.0f, 0.0f, 0.0f, 0.5f); // цвет фона - черный

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

  glDepthFunc (GL_LEQUAL);              // Тип проверки глубины (меньше или равно)

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

  glShadeModel (GL_SMOOTH);             // выберем плавное сглаживание

  glHint (GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  // установим вычисление перспективы в режим наибольшей точности

  glEnable(GL_TEXTURE_2D);              // включим отрисовку текстуры

  glBlendFunc(GL_ONE,GL_SRC_ALPHA);     // зададим режим смешения (простой и быстрый)

  glEnable(GL_BLEND);                  // включим смешение

 

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

 

for (int loop=0; loop<50; loop++)  // цикл для инициализации 50 объектов

  {

    SetObject(loop);            // вызов SetObject для задания новых случайных значений

  }

 

  return TRUE;     // возврат TRUE (успешная инициализация)

}

 

void Deinitialize (void)                  // все деинициализации пользователя находятся здесь

{

}

 

void Update (DWORD milliseconds)      // здесь происходит перерасчет движения

{

  if (g_keys->keyDown [VK_ESCAPE] == TRUE)  // была ли нажата клавиша ESC?

  {

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

  }

 

  if (g_keys->keyDown [VK_F1] == TRUE)              // была ли нажата клавиша F1?

  {

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

  }

}

 

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

 

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

 

Перво-наперво мы очищаем экран и в цикле отрисовываем все наши 50 бабочек (обьектов).

 

void Draw (void)                    // отрисовка сцены

{

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

 

  for (int loop=0; loop<50; loop++)              // цикл по 50 (рисуем 50 обьектов)

  {

 

Вызываем glLoadIdentity() для сброса матрицы просмотра модели. Затем мы выбираем текстуру, которая была присвоена нашему объекту (obj[loop].tex).

 

Поместим бабочку с помощью glTranslatef() и повернем ее на 45 градусов по оси Х. В этом случае бабочка будет наклонена слегка вперед к зрителю, чтобы не было эффекта плоского 2-мерного объекта.

 

Заключительное вращение закрутит бабочку по оси Z, пока она будет падать вниз на экране.

 

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

glBindTexture(GL_TEXTURE_2D, texture[obj[loop].tex]); // связывание с нашей текстурой    glTranslatef(obj[loop].x,obj[loop].y,obj[loop].z);    // помещение объекта    glRotatef(45.0f,1.0f,0.0f,0.0f);            // вращение по оси X

glRotatef((obj[loop].spinz),0.0f,0.0f,1.0f);          // вращение по оси Z

 

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

 

В коде, приведенном ниже, мы рисуем первый треугольник. Начинаем с верхнего правого угла невидимого квадрата. Затем двигаемся влево, пока не достигнем верхнего левого угла. Отсюда идем в нижний левый угол.

 

Код ниже отрисует следующее изображение:

 

 

Заметьте, что половина бабочки отрисована на первом треугольнике. Другая половина отрисована на втором треугольнике. Координаты текстуры совпадают с координатами вершин и, хотя задается всего 3 координаты для текстуры, но имеется достаточно информации, чтобы OpenGL определил какая порция изображения должна быть отрисована в треугольнике.

 

glBegin(GL_TRIANGLES);                // начала рисования треугольников

// первый треугольник

glTexCoord2f(1.0f,1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);      // точка 1 (верхняя правая)

glTexCoord2f(0.0f,1.0f); glVertex3f(-1.0f, 1.0f, obj[loop].flap);  // точка 2 (верхняя левая)

glTexCoord2f(0.0f,0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);      // точка 3 (нижняя левая)

 

 

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

 

Вторая точка первого треугольника и третья точка второго треугольника движутся вперед-назад по оси Z, что создает иллюзию хлопающих крыльев. На самом деле точки движутся из –1.0f в +1.0f и обратно, из-за этого треугольники сгибаются в центре, где находится тело бабочки.

 

Если посмотреть на оба изображения можно заметить, что точки 2 и 3 это кончики крыльев. Получается очень красивый эффект машущих крыльев с минимальным набором команд.

 

// второй треугольник

glTexCoord2f(1.0f,1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);      // точка 1 (вверху справа)

glTexCoord2f(0.0f,0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);      // точка 2 (внизу слева)

glTexCoord2f(1.0f,0.0f); glVertex3f( 1.0f,-1.0f, obj[loop].flap);  // точка 3 (внизу справа)

 

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

 

В коде, приведенном ниже, вычитание obj[loop].yi из obj[loop].y приведет к тому, что бабочка будет двигаться вниз по экрану. Значение переменной spinz увеличивается на величину spinzi (которое может быть как положительным, так и отрицательным), а крылья увеличиваются на величину fi. fi также может иметь отрицательное или положительное значения в зависимости от того, в каком направлении мы хотим, чтобы хлопали крылья.

 

obj[loop].y-=obj[loop].yi;              // двигаем объект вниз по экрану

obj[loop].spinz+=obj[loop].spinzi;      // увеличиваем вращение по Z на величину spinzi

obj[loop].flap+=obj[loop].fi;           // увеличиваем значение хлопанья на fi

 

После перемещения бабочки вниз по экрану надо проверить, не вышла ли она за пределы экрана (она больше невидима). Если это произошло, вызываем SetObject(loop) для задания этой бабочке новой текстуры, новой скорости и т.д.

    if (obj[loop].y<-18.0f)         // объект вне экрана?

    {

      SetObject(loop);              // если да – перезадай новые параметры

 

 

Чтобы крылья махали, мы проверяем значение хлопанья на диапазон +1.0f -1.0f. Если значение вне диапазона, то мы меняем направление хлопанья: fi=-fi. Таким образом, если крыло идет вверх и достигает значения 1.0f, то fi принимает отрицательное значение и крыло идет вниз.

 

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

 

if ((obj[loop].flap>1.0f) || (obj[loop].flap<-1.0f)) // изменяем направление хлопанья?

    {

      obj[loop].fi=-obj[loop].fi;            // сменим направление fi = -fi

    }

  }

 

  Sleep(15);                    // короткая задержка (15 Milliseconds)

 

  glFlush ();                    // сброс линии отрисовки GL

}

 

 

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

 

Спасибо всем за отличную поддержку! Этот сайт был бы ничем без его посетителей!

 

© Jeff Molofee (NeHe)

PMG  06 февраля 2005 (c)  Геннадий Хохорин