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

Volumetric Fog & IPicture Image Loading

 

Вашему вниманию предлагается еще один занимательный урок. На этот раз я буду пытаться объяснить, что такое объемный туман, используя расширение glFogCoordf. Чтобы запустить эту демонстрационную версию, ваша видео плата должна поддержать расширение "GL_EXT_FOG_COORD". Если Вы не уверены, поддерживает ли ваша плата это расширение, у вас есть два пути: Первое скачать исходники на VC++, и посмотреть, выполняется ли он. Второе скачать урок 24, и просмотреть список расширений, поддерживаемых вашей видео платой.

 

В этом уроке я познакомлю вас с кодом NeHe IPicture, который способен загружать BMP, EMF, GIF, ICO, JPG и WMF файлы с вашего компьютера или web-странички. Вы также узнаете, как использовать расширение "GL_EXT_FOG_COORD", чтобы создавать круто смотрящийся  объемный туман (туман, который может перемещаться в замкнутом пространстве, не изменяя остальную  часть сцены).

 

Если этот урок не работает на вашей машине, первом делом, вы должны проверить, что у вас стоит самый свежий видеодрайвер. Если же у вас драйвер самый новый, а демка по-прежнему не работает, то … вы может быть захотите купить новую видео плату. Начиная с GeForce 2 все будет прекрасно работать, и стоит не дорого. Если ваша плата не поддерживает расширение для вывода объемного тумана, кто говорит, что она не поддерживает и другие расширения?

 

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

 

Сказано – сделано! Давайте проштудируем кое-какой код!!!

 

Начало кода очень похоже на основу старого, и почти идентично новому коду NeHeGL. Единственное отличие - дополнительная строка кода, подключающая библиотеку OLECTL. Этот заголовочный файл должен быть включен, если Вы хотите, чтобы код IPicture функционировал. Если Вы не напишете эту строчку, то у вас возникнут ошибки при попытке использовать IPicture, OleLoadPicturePath и IID_IPicture.

 

Точно так же как и в базовом коде NeHeGL, мы используем комментарий *pragma (lib...) чтобы автоматически включить требуемые библиотечные файлы! Заметьте, нам не надо больше включать glaux библиотеку (я – уверен, многие из вас сейчас улыбаются).

 

Следующие три строчки кода проверяют, определено ли значение CDS_FULLSCREEN. Если нет (что верно для большинства трансляторов), мы задаем ему значение 4. Я знаю, что многие из вас писали мне об этом по электронной почте, чтобы узнать о причине ошибок при  компилировании кода, используя CDS_FULLSCREEN в DEV C ++. Добавьте эти три строки, и ошибок у вас не будет!

 

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

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

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

#include <vfw.h>             // Заголовочный файл для «Видео для Windows»

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

 

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

                             // управляющих элементов OLE (используется в BuildTexture)

#include <math.h>            // Заголовочный файл для математической библиотеки

                             // (используется в BuildTexture)

 

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

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

 

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

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

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

 

GL_Window*  g_window;                 // структура окна

Keys*    g_keys;                      // клава

 

В следующей части кода, мы устанавливаем цвет нашего тумана. В данном случае мы хотим, чтобы он был темно-оранжевого цвета. Небольшое количество красного (0.6f), смешанного с еще меньшим количеством зеленого (0.3f) даст нам цвет, который мы хотим.

 

Переменная camz типа Glfloat будет использована позже, чтобы определять позицию нашей камеры внутри длинного и темного коридора! Мы будем перемешаться вперед и назад вдоль коридора, выполняя преобразования на оси Z, перед тем как выполнить рисование.

 

// Наши переменныеs

GLfloat  fogColor[4] = {0.6f, 0.3f, 0.0f, 1.0f}; // цвет тумана

GLfloat camz;                                    // «глубина» камеры по Z

 

Точно так же как CDS_FULLSCREEN имеет предопределенное значение 4, переменные GL_FOG_COORDINATE_SOURCE_EXT и GL_FOG_COORDINATE_EXT также имеют предопределенные значения. Как упомянуто в комментариях, значения были взяты из заголовочного файла GLEXT. Его можно свободно скачать из сети. Огромное спасибо Льву Повалаеву за создания такого ценного заголовочного файла! Эти значения должны быть установлены, если вы хотите, чтобы код компилировался! Результатом является то, что, мы имеем два новых списка перечислений (GL_FOG_COORDINATE_SOURCE_EXT и GL_FOG_COORDINATE_EXT).

 

Чтобы использовать функцию glFogCoordfExt, мы должны объявить прототип функции через typedef, чтобы наша функция соответствовала точке входа расширения. А если по-русски, то нам надо сообщить нашей программе число параметров и тип каждого параметра, которые подаются на вход функции glFogCoordfExt. В данном случае, мы передаем один параметр для этой функции, и это - переменная с плавающей точкой.

 

Затем мы должны объявить глобальную переменную нашего типа (PFNGLFOGCOORDFEXTPROC). Это - первый шаг создания нашей новой функции (glFogCoordfEXT). Эта переменная объявлена глобальной, для того чтобы мы смогли использовать команду где - угодно в нашем коде. Название, которое мы используем, должно точно соответствовать фактическому имени расширения. Фактическое имя расширения - glFogCoordfEXT и название, которое мы используем - также glFogCoordfEXT.

 

Однажды использовав wglGetProcAddress, чтобы присвоить переменной определенного выше типа адрес функции расширения драйвера OpenGL, мы можем вызывать glFogCoordfExt, как если бы это была обычная функция. Подробнее об этом после!

 

Последняя строка подготавливает все необходимое для нашей единственной текстуры.

 

Так, что пока мы имеем вот что ...

 

Мы знаем, что PFNGLFOGCOORDFEXTPROC принимает одно значение с плавающей точкой (GLfloat coord). Поскольку glFogCoordfExt – имеет тип PFNGLFOGCOORDFEXTPROC, то вызов этой функции будет выглядеть вот так: glFogCoordfEXT (GLfloat coord). Наша функция определена, но не будет ничего делать, потому что glFogCoordfExt – NULL (мы по-прежнему должны прикрутить glFogCoordfExt к адресу функции расширения OpenGL драйвера).

 

Надеюсь, что это понятно… на самом деле, все очень просто, когда вы уже знаете, как это работает... но описывать это чрезвычайно трудно (по крайней мере, для меня, это так). Если кто-либо может переписать этот раздел урока, используя простую и не сложную формулировку, пришлите ее мне! Единственный способ, с помощью которого я смог объяснить это лучше использовать иллюстрации, но сейчас я спешу выложить этот урок в сеть!

 

// переменные необходимые для FogCoordfEXT

#define GL_FOG_COORDINATE_SOURCE_EXT  0x8450          // значение из GLEXT.H

#define GL_FOG_COORDINATE_EXT    0x8451               // значение из GLEXT.H

 

typedef void (APIENTRY * PFNGLFOGCOORDFEXTPROC) (GLfloat coord); // прототип функции

 

PFNGLFOGCOORDFEXTPROC glFogCoordfEXT = NULL; // наша функция glFogCoordfEXT

 

GLuint  texture[1]; // единственная текстура для стен

 

Ну, а теперь, чтобы было веселее, фактический код, который превращает изображение в текстуру, использует волшебство IPicture :).

 

Эта функция требует путь (путь к фактическому изображению, которое мы хотим загрузить, или имя файла, или URL в сети) и идентификатор текстуры (например... texture [0]).

 

Мы должны создать контекст устройства для нашего временного растра. Нам также необходимо иметь указатель на место в памяти для хранения растра (hbmpTemp), в указетель на интерфейс IPicture, а также строковую переменную для хранения пути (до файла или URL). Еще нам понадобится по 2 переменных для хранения ширины и высоты изображения. Переменные lwidth и lheight хранят фактическую ширину и высоту изображения. Переменные lwidthpixels и lheightpixels хранят ширину и высоту в пикселях, откорректированных, чтобы исправить максимальный размер текстуры, который может отобразить видеокарта. Максимальный размер текстуры будет храниться в glMaxTexDim.

 

int BuildTexture(char *szPathName, GLuint &texid)

   // читаем изображение и преобразуем его в текстуру

{

  HDC    hdcTemp;                        // DC для растра

  HBITMAP    hbmpTemp;            // иногда храним в ней растр

  IPicture  *pPicture;            // интерфейс IPicture

  OLECHAR    wszPath[MAX_PATH+1]; // полный путь до картинки (WCHAR)

  char    szPath[MAX_PATH+1];     // полный путь до картинки

 long    lWidth;                  // ширина в логических единицах

  long    lHeight;                // высота в логических единицах

  long    lWidthPixels;           // ширина в пикселях

  long    lHeightPixels;          // высота в пикселях

  GLint    glMaxTexDim ;          // максимальный размер текстуры

 

Следующий раздел кода берет имя файла и проверяет его, является ли он URL’ом или путем до файла. Мы делаем это, путем проверки, содержит ли имя файла http://. Если имя файла - URL, мы копируем имя в szPath.

 

Если имя файла не содержит URL, мы получаем текущий каталог. Если вы сохранили демонстрационную версию в C:\wow\lesson41, и пробовали загружать data\wall.bmp, программе нужно знать полный путь до wall.bmp файла, а не только то, что bmp файл сохранен в папке ‘data’. GetCurrentDirectory найдет текущий путь, т.е. путь до папки, где лежит .EXE файл и  папка 'data'.

 

Если .exe был сохранен в "c:\wow\lesson41", то текущая директория была бы "c:\wow\lesson41". Мы должны добавить "\\" к концу пути до текущего каталога, а затем "data\wall.bmp". "\\"  - это просто "\". Если мы все это соединим "c:\wow\lesson41" + "\" + "data\wall.bmp", то у нас получится "c:\wow\lesson41\data\wall.bmp". Теперь понятнее?

 

  if (strstr(szPathName, "http://")) // Если путь содержит http:// то...

  {

    strcpy(szPath, szPathName); // прикрутить PathName к szPath

  }

  else // иначе загрузить из файла

  {

    GetCurrentDirectory(MAX_PATH, szPath); // дайте каталог, где мы сидим

    strcat(szPath, "\\");                  // добавим к концу пути ‘\’

    strcat(szPath, szPathName);            // приклеим PathName

  }

 

Так что у нас есть сейчас полный путь в переменной szPath. Теперь нам надо его перекинуть из ASCII в Unicode так, чтобы OleLoadPicturePath воспринял его. Первая строчка кода делает это за нас. Результат сохраняется в wszPath.

 

CP_ACP означает кодовую страницу ANSI. Второй параметр определяет обработку не отображаемых символов (далее по коду мы его игнорируем). Символы в строке szPath будут конвертированы в расширенные  символы. 4-ый параметр – длина строки с расширенными  символами. Если это значение установлено в -1, то считается, что строка заканчивается нулевым символом (NULL). wszPath – то место, куда будет сохранена сконвертированная строка, и MAX_PATH - максимальный размер пути до файла (256 символов).

 

После преобразования пути в Unicode, мы попытаемся загрузить изображение, используя OleLoadPicturePath. Если все хорошо, то pPicture будет указывать на данные изображения, и код результата будет сохранен в hr.

 

Если загрузка не пройдет, то программа прекратит свою работу.

 

  MultiByteToWideChar(CP_ACP, 0, szPath, -1, wszPath, MAX_PATH);

  // преобразуем к юникоду

  HRESULT hr = OleLoadPicturePath(wszPath, 0, 0, 0, IID_IPicture, (void**)&pPicture);

 

  if(FAILED(hr))  // Если загрузка не удачна

    return FALSE; // вернем False

 

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

 

  hdcTemp = CreateCompatibleDC(GetDC(0)); // создать совместимый с устройством Windows контекст

  if(!hdcTemp)            // ну что, создали?

  {                       // не-а… :(

    pPicture->Release();  // уменьшение счетчика ссылок на IPicture

    return FALSE;         // солгать

  }

 

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

 

Что касается кода, мы используем glGetIntegerv (...), чтобы получить максимальное разрешение текстуры (256, 512, 1024, и т.д.) поддерживаемое конкретной видеокартой. Затем мы проверяем фактический размер изображения с помощью: pPicture-> get_width (&lwidth).

 

Мы используем причудливые вычисления, чтобы преобразовать ширину изображения в пиксели. Результат хранится в lwidthpixels. Тоже самое делаем с высотой. Мы получаем высоту изображения от pPicture и сохраняем значения пикселя в lheightpixels.

 

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

  glGetIntegerv(GL_MAX_TEXTURE_SIZE, &glMaxTexDim);

 

  pPicture->get_Width(&lWidth); // получить ширину изображения

  lWidthPixels  = MulDiv(lWidth, GetDeviceCaps(hdcTemp, LOGPIXELSX), 2540);

  pPicture->get_Height(&lHeight); // получить высоту изображения

  lHeightPixels  = MulDiv(lHeight, GetDeviceCaps(hdcTemp, LOGPIXELSY), 2540);

 

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

 

Если ширина изображения в пикселях - меньше чем максимальная поддерживаемая ширина, мы делаем размер изображения кратной степени двойки, исходя из текущей ширины изображения в пикселях. Мы добавляем 0.5f, чтобы изображение всегда было больше, если его размер близок к следующему размеру. Например, если наша ширина изображения была 400, и видео плата поддержала максимальную ширину 512, то было бы лучше сделать ширину 512. Если бы мы сделали ширину 256, то изображение бы многое утратило, но подробнее об этом позже.

 

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

 

То же самое повторяем и для высоты. В итоге ширина и высота изображения будут сохранены в lwidthpixels и lheightpixels.

 

  // преобразовать изображение к ближайшей степени двойки

  if (lWidthPixels <= glMaxTexDim)

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

    lWidthPixels = 1 << (int)floor((log((double)lWidthPixels)/log(2.0f)) + 0.5f);

  else

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

    // которую поддерживает карточка

    lWidthPixels = glMaxTexDim;

  // то же самое повторяется для высоты

  if (lHeightPixels <= glMaxTexDim)

    lHeightPixels = 1 << (int)floor((log((double)lHeightPixels)/log(2.0f)) + 0.5f);

  else

    lHeightPixels = glMaxTexDim;

 

Теперь, когда мы загрузили данные изображения, и знаем высоту и ширину, которая нам нужна, мы должны создать временный растр. Переменная bi будет содержать заголовочную информацию растра, а pBits будет хранить фактические данные изображения. Мы хотим создать  32-х битный растр с шириной lwidthpixels и высотой lheightpixels., а также, чтобы изображения было в формате RGB, и чтобы изображение имело одну битовую плоскость.

 

  // создать временный растр

  BITMAPINFO  bi = {0}; // нужный нам тип растра

  DWORD    *pBits = 0;  // указатель на биты растра

 

  bi.bmiHeader.biSize     = sizeof(BITMAPINFOHEADER); // размер структуры

  bi.bmiHeader.biBitCount = 32; // 32 бита

  bi.bmiHeader.biWidth    = lWidthPixels;  // ширина кратная степени двойки

  // Сделаем изображение расположенным вверх (положительное направление оси Y)

  bi.bmiHeader.biHeight   = lHeightPixels;

  bi.bmiHeader.biCompression  = BI_RGB;    // RGB формат

  bi.bmiHeader.biPlanes    = 1;            // 1 битовая плоскость

 

MSDN сообщает: функция CreateDIBSection создает DIB (независимый от устройства растр), куда программы могут писать напрямую. Функция возвращает вам указатель на память, где хранятся битовые значения растра. Вы можете позволить системе выделить память для растра.

 

hdcTemp - наш временный контекст устройства. bi - наша инфа о растре (информация заголовка). DIB_RGB_COLORS говорит нашей программе: “Мы хотим хранить RGB данные, а не индексы в логической палитре!” (каждый пиксель будут иметь красное, зеленое и синее значения).

 

pBits - то, куда данные изображения будут сохранены (указывает на данные изображения). Последние два параметра можно опустить.

 

Если программа не смогла создать временный растр, мы “заметаем следы” (чистим память) и возвращаем ЛОЖЬ, чтобы завершилась программа.

 

Если все тип-топ, то мы сводим счеты с временным растром. Мы используем SelectObject, чтобы прикрутить растр к временному контексту устройства.

 

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

  // а также получить прямой доступ к битам.

  hbmpTemp = CreateDIBSection (hdcTemp, &bi, DIB_RGB_COLORS, (void**)&pBits, 0, 0);

 

  if(!hbmpTemp)        // создали?

  {                    // сам вижу что нет

   DeleteDC(hdcTemp);  // убить контекст устройства

  pPicture->Release(); // уменьшить счетчик количества интерфейсов IPicture

    return FALSE;      // вернуть ЛОЖЬ

  }

  // есть растр!

  SelectObject(hdcTemp, hbmpTemp); // загрузить описатель временного растра

                                   // в описатель временного контекста устройства

 

Теперь нам надо заполнить наш растр данными нашего изображения. pPicture->Render сделает это для нас. При этом также изменятся размеры изображения к любому размеру, который мы пожелаем (в этом случае lwidthpixels на lheightpixels).

 

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

 

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

 

Следующий параметр (равный нулю) - горизонтальное смещение, от которого мы хотим читать исходные данные. Мы рисуем слева направо, так что смещение равно нулю. Это станет понятным после того, как вы увидите, что мы делаем с вертикальным смещением (будем надеяться, по крайней мере).

 

Параметр lHeight - вертикальное смещение. Мы будем читать данные снизу исходного изображения до верха, используя смещение lHeight, мы начинам с самого нижнего края исходного изображения.

 

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

 

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

 

Последний параметр не используется.

 

  // отрисовка IPicture в растр

  pPicture->Render(hdcTemp, 0, 0, lWidthPixels, lHeightPixels, 0, lHeight, lWidth, -lHeight, 0);

 

Теперь у нас есть растр с шириной lWidthpixels и высотой lHeightpixels. Новый растр был зеркально перевернут.

 

К сожалению, данные сохранены в формате BGR. Так что мы должны поменять красные и синие пиксели, чтобы создать растр и изображение RGB. В то же самое время, мы устанавливаем значение альфа-канала в 255. Вы можете изменить это значение на что угодно.

 

  // преобразовать из BGR в RGB формат и устанавливаем значение

  // Alpha = 255

  for(long i = 0; i < lWidthPixels * lHeightPixels; i++) // Цикл по всем пикселям

  {

    BYTE* pPixel  = (BYTE*)(&pBits[i]); // берем текущий пиксель

    BYTE  temp  = pPixel[0];            // сохраняем первый цвет в  переменной

                                        // Temp (Синий)

    pPixel[0]  = pPixel[2];             // ставим  Красный на место (в первую позицию)

    pPixel[2]  = temp;                  // ставим значение Temp в третий параметр (3rd)

    pPixel[3]  = 255;                   // установить значение alpha =255

  }

 

Наконец, после всей этой работы, у нас есть растр, который может использоваться как текстура. Мы связываем его с texid, и генерируем текстуру. Мы хотим использовать линейную фильтрацию для min и mag фильтров (классно смотрится).

 

Мы получаем данные изображения от pBits. При создании текстуры, мы используем lwidthpixels и lheightpixels в последний раз, чтобы установить ширину и высоту текстуры.

 

После того, как 2D текстура была сгенерирована, мы можем освободить память. Нам больше не нужен ни временный растр, ни временный контекст устройства. Удаляем их. А еще мы можем освободить pPicture! Еууу! :)))

 

  glGenTextures(1, &texid); //  создаем текстуру

 

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

  glBindTexture(GL_TEXTURE_2D, texid); // делаем привязку к texid

  // (измените для нужного вам типа фильтрации)

  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

  // (измените, если хотите использовать мипмап-фильтрацию) 

  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

 

  // (мипмап - множественное отображение (последовательность текстур одного

  //  и того же изображения с уменьшающимся разрешением по мере удаления отображаемого

  // объекта от наблюдателя))

 

  glTexImage2D(GL_TEXTURE_2D, 0, 3, lWidthPixels, lHeightPixels, 0, GL_RGBA,

               GL_UNSIGNED_BYTE, pBits);

 

  DeleteObject(hbmpTemp); // удаляем объект

  DeleteDC(hdcTemp);      // удаляем контекст устройства

 

  pPicture->Release();    //уменьшает счетчик IPicture

 

  return TRUE;            // вернуть ПРАВДУ (все ОК)

}

 

Следующий код проверяет, поддерживает ли пользовательская видео плата расширение EXT_FOG_coord. Этот код может вызываться ТОЛЬКО после того, как ваша OpenGL программа имеет контекст для визуализации. Если Вы попробуете вызвать это раньше, чем вы создадите окно, то у вас будут ошибки.

 

Первое, что мы делаем - создаем строку с именем нашего расширения.

 

Затем мы выделяем достаточное количество памяти, чтобы хранить список OpenGL расширений, поддерживаемых пользовательской видеоплатой. Список поддерживаемых расширений - извлекается командой glGetString (GL_EXTENSIONS). Возвращенная информация копируется в glextstring.

 

Как только мы получаем список поддерживаемых расширений, мы используем strstr, чтобы определить, находится ли наше расширение (Extension_Name) в списке поддерживаемых расширений (glextstring).

 

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

 

int Extension_Init()

{

  char Extension_Name[] = "EXT_fog_coord";

 

  // выделяем память для строки расширения

  char*glextstring=(char*)malloc(strlen((char*)glGetString(GL_EXTENSIONS))+1);

  // копируем список расширений в glextstring

  strcpy (glextstring,(char *)glGetString(GL_EXTENSIONS));

 

  if (!strstr(glextstring,Extension_Name)) // поддерживается ли расширение??

    return FALSE; // если нет – то вернуть FALSE

 

  free(glextstring); // иначе освобождаем память

 

В самом начале этой программы мы определили glFogCoordfExt. Однако команда не будет работать, пока мы не присоединим функцию к фактическому OpenGL расширению. Мы делаем это, передавая glFogCoordfExt адрес расширения тумана OpenGL. Когда мы вызываем  glFogCoordfExt, фактический код расширения выполнится, и мы получим параметр, переданный glFogCoordfExt.

 

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

 

  // устанавливаем и активируем glFogCoordEXT

  glFogCoordfEXT = (PFNGLFOGCOORDFEXTPROC) wglGetProcAddress("glFogCoordfEXT");

  return TRUE;

}

 

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

 

До этого наша программа должна иметь RC (контекст рендеринга). Это важно, потому что вы должны иметь контекст рендеринга прежде, чем вы можете проверить, поддерживаться ли расширение конкретной видеоплатой.

 

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

 

Если расширение поддерживается, мы попытаемся загрузить нашу текстуру wall.bmp. Идентификатором для этой текстуры будет texture[0]. Если по каким-либо причинам текстура не загружается, программа завершится.

 

Инициализация проста. Мы активизируем двухмерное наложение текстуры и задаем черный цвет очистки экрана. Очищаем буфер глубин 1.0f. Мы задаем тест глубины типа «меньше или равно» и разрешаем тест глубины. Модель закрашивания устанавливаем равным плавному закрашиванию, и мы выбираем наилучшую коррекцию перспективы.

 

BOOL Initialize (GL_Window* window, Keys* keys)

     // код инициализации

{

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

  g_keys    = keys;   // ключевые значения

 

  // начало пользовательской инициализации

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

  if (!Extension_Init())

    return FALSE; // вернуть False если расширение не поддерживается

  if (!BuildTexture("data/wall.bmp", texture[0])) // загрузить текстуру стены

    return FALSE; // вернуть False если загрузка не прошла

 

  glEnable(GL_TEXTURE_2D);   // вкл отображение текстуры

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

  glClearDepth (1.0f);       // установить глубину буфера

  glDepthFunc (GL_LEQUAL);   // тип теста глубины

  glEnable (GL_DEPTH_TEST);  // вкл тест глубины

  glShadeModel (GL_SMOOTH);  // выбрать плавное закрашивание

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

  glHint (GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

 

Ну, а, теперь можно по-прикалываться. Мы должны задать туман. Начинаем с активизации тумана. Режим визуализации, который мы используем, линейный. Цвет тумана установлен с помощью fogColor (оранжевый).

 

Затем мы должны установить стартовую позицию тумана. Это - наименее плотная часть тумана. Для простоты, мы будем использовать 1.0f как наименьшее значение плотности (FOG_START). Мы будем использовать 0.0f как наибольшее значение плотности тумана (FOG_END).

 

Согласно документации, которую я прочел, настраивая туман, используя GL_NICEST, он будет отображаться по-пиксельно. При использовании GL_FASTEST туман будет выводиться по вершинам. Лично я не вижу никакой разницы.

 

Последняя  команда glFogi (...) сообщает OpenGL, что мы хотим установить наш туман, используя координаты вершин. Это позволяет устанавливать туман в любом месте нашей сцены ни влияя на всю сцену (улет!).

 

Мы устанавливаем начальное значение camz в -19.0f. Коридор на самом деле имеет длину 30 единиц. Поэтому -19.0f перемещает нас, почти в начало коридора (коридор визуализируется от -15.0f до +15.0f по оси Z).

 

  // устанавливаем туман

  glEnable(GL_FOG);                // активизируем туман

  glFogi(GL_FOG_MODE, GL_LINEAR);  // затенение линейно

  glFogfv(GL_FOG_COLOR, fogColor); // устанавливаем цвет тумана

  glFogf(GL_FOG_START,  0.0f);     // устанавливаем начальную плотность тумана

  glFogf(GL_FOG_END,    1.0f);     // устанавливаем конечную плотность тумана

  glHint(GL_FOG_HINT, GL_NICEST);  // вычисляем туман по пикселям

  // делаем туман на координатах вершин

  glFogi(GL_FOG_COORDINATE_SOURCE_EXT, GL_FOG_COORDINATE_EXT);

 

  camz =  -19.0f; // устанавливаем значение камеры по оси Z = -19.0f

 

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

}

 

Эта часть кода вызывается всякий раз, когда пользователь выходит из программы. Мусора у нас нет, поэтому и кода нет :-).

 

void Deinitialize (void)              // код выхода

{

}

 

Тут мы обрабатываем клавиатуру. Подобно всем предыдущим урокам, мы проверяем, нажата ли кнопка ESC. Если да, то приложение закрывается. Если нажата клавиша F1, мы переключаемся из полноэкранного режима в  оконный  режим или из оконного в полноэкранный.

 

Другие две кнопки, которые мы проверяем – это стрелки «вверх» и «вниз». Если кнопка «вверх» нажата, и значение camz - меньше чем 14.0f, мы увеличиваем camz. При этом наблюдатель будет перемещаться вперед по коридору. Если бы значение было больше 14.0f, мы бы прошли через заднюю стену. Но мы не хотим, чтобы это произошло :).

 

Если нажата кнопка «вниз», и значение camz большее чем -19.0f, мы уменьшаем camz. При этом наблюдатель будет  перемещаться назад по коридору. Если бы  значение было меньше -19.0f, коридор был бы слишком далек от экрана, и виден был бы только вход в коридор. Опять же... Это было бы не хорошо!

 

Значение camz увеличивается и уменьшается на число прошедших миллисекунд, деленных на 100.0f. Это должно заставить программу выполняться с одинаковой скоростью на всех типах процессоров.

 

void Update (DWORD milliseconds)     // выполняем обновление движения

{

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

    TerminateApplication (g_window); // прекращаем работу

 

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

    ToggleFullscreen (g_window); // переход в полноэкранный режим или в оконный

 

  if (g_keys->keyDown [VK_UP] && camz<14.0f) // стрелка вверх нажата?

    camz+=(float)(milliseconds)/100.0f;     // приближаем объект

 

  if (g_keys->keyDown [VK_DOWN] && camz>-19.0f) // стрелка вниз нажата?

    camz-=(float)(milliseconds)/100.0f;         // отдаляем объект

}

 

Я уверен, что вы готовы умереть, чтобы получить визуализацию, но нам надо кое-что еще сделать, чтобы отобразить коридор. Сначала мы должны очистить экран и буфер глубины. Мы сбрасываем матрицу вида модели и сдвигаем камеру на значение camz.

 

Увеличивая или уменьшая значение camz, коридор станет ближе или дальше от наблюдателя. Это создаст впечатление, что наблюдатель двигается вперед или назад через коридор. Все гениальное просто!

 

void Draw (void)

{

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

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

  glTranslatef(0.0f, 0.0f, camz); // устанавливаем Z координату камеры

 

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

 

Мы хотим, чтобы эта стенка была в самом плотном слое тумана. Если Вы взглянете на код инициализации, вы увидите, что GL_FOG_END - наиболее плотная часть тумана... и значение его 1.0f.

 

Туман устанавливается тем же самым способом, каким вы управляете координатами текстуры. Значение GL_FOG_END является самым плотным туманом и имеет значение 1.0f. Поэтому для вершин нашего первого полигона мы передаем в glFogCoordfExt значение 1.0f. Этот полигон задает самую дальнюю стену (стену, которую вы будете наблюдать в конце тоннеля) координаты по осям X и Y равны -2.5f. Там будет наиболее плотный туман.

 

  glBegin(GL_QUADS); // задняя стена

    glFogCoordfEXT(1.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-2.5f,-2.5f,-15.0f);

    glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f( 2.5f,-2.5f,-15.0f);

    glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f( 2.5f, 2.5f,-15.0f);

    glFogCoordfEXT(1.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-2.5f, 2.5f,-15.0f);

  glEnd();

 

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

 

Подобно всем квадратам, пол имеет 4 точки. Значение Y всегда равно -2.5f. Значение по оси X левой вершины равно -2.5f, а правой вершины -2.5f, и пол рисуется по оси Z от  -15.0f до +15.0f.

 

Нам нужно, чтобы дальняя часть пола находилась в тумане. Снова присвоим вершинам glFogCoordfExt значение 1.0f. Заметьте, что любая вершина, нарисованная со значением по оси Z равным -15.0f имеет значение glFogCoordfExt равным 1.0f...?!

 

Часть пола, ближайшая к наблюдателю (+15.0f) будет иметь наименьшую плотность тумана. GL_START_FOG - наименее плотный туман и имеет значение 0.0f. Так что, для этих точек присвоим  переменой glFogCoordfExt  значение 0.0f.

 

То, что вы должны видеть, когда запустите программу, это на самом деле плотный туман на полу возле задней части и светлый туман спереди. Туман недостаточно густой, чтобы заполнить весь коридор. В действительности, он рассеивается где-то в середине коридора, даже если GL_START_FOG = 0.0f.

 

  glBegin(GL_QUADS); // пол

    glFogCoordfEXT(1.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-2.5f,-2.5f,-15.0f);

    glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f( 2.5f,-2.5f,-15.0f);

    glFogCoordfEXT(0.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f( 2.5f,-2.5f, 15.0f);

    glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-2.5f,-2.5f, 15.0f);

  glEnd();

 

Потолок выводится так же, как и пол, с единственным отличием в том, что потолок рисуется по оси Y со значением координаты равным 2.5f.

 

  glBegin(GL_QUADS); // потолок

    glFogCoordfEXT(1.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-2.5f, 2.5f,-15.0f);

    glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f( 2.5f, 2.5f,-15.0f);

    glFogCoordfEXT(0.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f( 2.5f, 2.5f, 15.0f);

    glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-2.5f, 2.5f, 15.0f);

  glEnd();

 

Правая стенка рисуется точно также. Только по оси X значение всегда 2.5f. Самые дальние точки на оси Z по-прежнему устанавливаются равными glFogCoordfExt (1.0f), а самые близкие по оси Z равны glFogCoordfExt (0.0f).

 

  glBegin(GL_QUADS); // правая стена

    glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f( 2.5f,-2.5f, 15.0f);

    glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f( 2.5f, 2.5f, 15.0f);

    glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f( 2.5f, 2.5f,-15.0f);

    glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f( 2.5f,-2.5f,-15.0f);

  glEnd();

 

Будем надеяться, теперь вы понимаете, как все это работает. Все что находится на расстоянии должно иметь значение 1.0f. Все что ближе - должно быть равно 0.0f.

 

В любом случае, вы можете поэкспериментировать со значениями GL_FOG_START и GL_FOG_END, чтобы увидеть, как они влияют на сцену.

 

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

 

Этот тип тумана смотрится лучше всего, когда игрок может видеть комнату с туманом, но фактически не может войти в нее. Хорошим примером может быть глубокая яма, закрытая чем-то наподобие решетки. Игрок может смотреть вниз, но не может попасть туда.

 

  glBegin(GL_QUADS); // левая стена

    glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-2.5f,-2.5f, 15.0f);

    glFogCoordfEXT(0.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-2.5f, 2.5f, 15.0f);

    glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f(-2.5f, 2.5f,-15.0f);

    glFogCoordfEXT(1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f(-2.5f,-2.5f,-15.0f);

  glEnd();

  glFlush ();

}

 

Я надеюсь, вам понравился этот урок. Он был создан в течение 3 дней... по 4 часа в день. Большинство времени было потрачено на написание текста, который Вы в данный момент читаете.

 

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

 

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

 

Эти урок показывает Вам, как использовать glFogCoordfExt. Это быстро, круто и легко! Стоит обратить внимание, что это - только ОДИН из многих различных способов создать объемный туман. Тот же самый эффект может быть создан, используя смешивание, частицы, маски, и т.д.

 

Как всегда... Если вы нашли ошибки в этом уроке, сообщите мне. Если Вы считаете, что Вы можете описать часть кода лучше (моя формулировка не всегда понятна), намыльте мне : ) !

 

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

 

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

Jeff Molofee (NeHe)

PMG  27 апреля 2004 (c)  Алексей Граков