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

Setting Up An OpenGL Window

Добро пожаловать в мои уроки по OpenGL. Я обычный программист, который любит OpenGL. Первый раз я услышал об OpenGL, когда 3DFx выпустила драйвера для OpenGL для видеокарты Voodoo 1. Тут же я понял, что OpenGL это то, чему я должен научиться. В это время было довольно сложно найти нужную информацию по OpenGL в книгах или Интернет. Я потратил много времени, чтобы сделать рабочий код и часто спрашивал про OpenGL по электронной почте или на каналах IRC. Я увидел, что те люди, которые знали OpenGL, считали себя элитой, и не хотели делиться своими знаниями. Что очень неприятно!

 

Этот сайт был создан для того, чтобы помочь людям, которые интересуются OpenGL. В других уроках я попытаюсь объяснить в деталях, на сколько это будет возможно, что делают отдельные строки кода. Я попытаюсь сделать код как можно проще (не используя MFC)! Так, чтобы даже абсолютный новичок в Visual C ++ и OpenGL был способен, прочитав код, понять то, что происходит. Этот сайт всего лишь один из многих предлагающих материал для изучения по OpenGL. Если Вы опытный программист OpenGL, то сайт может показаться слишком простым. Но если Вы только начинаете, я надеюсь, что этот сайт многое может Вам предложить!

 

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

 

Я начинаю этот урок непосредственного с кода, который разбит на секции, каждая из которых будет подробно комментироваться. Первое, что Вы должны сделать - это создать проект в Visual C++. Если это для Вас затруднительно, то Вам стоит для начала изучить C++, а уже затем переходить на OpenGL. При написании был использован компилятор MicroSoft Visual Studio 2005. Некоторые версии VC++ требуют замены bool на BOOL, true на TRUE и false на FALSE.

 

После создания нового Win32 приложения (НЕ КОНСОЛЬНОГО) в Visual C++, Вам надо будет добавить для сборки проекта библиотеки OpenGL. В меню Project/setting, выберите закладку LINK. В строке "Object/Library Modules" добавьте "OpenGL32.lib GLu32.lib GLaux.lib". Затем нажмите OK. Теперь все готово для создания программы на OpenGL.

 

Примечание #1: Во многих компиляторах константа CDS_FULLSCREEN - не определенна. Если получено сообщение об ошибке связанное с CDS_FULLSCREEN, то Вы должны добавить следующую строчку в начале кода Вашей программы: #define CDS_FULLSCREEN 4.

 

Примечание #2: Когда писался первый урок, библиотека GLAUX была такой, какой ей и следовало оставаться. Со временем её перестали поддерживать. До сих пор во многих уроках на этом сайте используется как раз прежний вариант библиотеки. Если Ваш компилятор не поддерживает GLAUX или Вы не желаете ее использовать, скачайте GLAUX REPLACEMENT CODE отсюда.

 

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

 

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

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

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

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

 

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

 

Первые строки устанавливают Контекст Рендеринга (Rendering Context). Каждая OpenGL программа связывается с Контекстом Рендеринга, который в свою очередь вызывает Контекст Устройства (Device Context). Контекст Рендеринга OpenGL определен как hRC. Для того чтобы рисовать в окне, Вам необходимо создать Контекст Устройства Windows, который определен как hDC. DC соединяет окно с GDI (Graphics Device Interface). RC соединяет OpenGL с DC.

 

В третьей строке инициализирована переменная hWnd, которая хранит дескриптор (уникальную ссылку-идентификатор) окна и, наконец, в четвёртой строке объявляется дескриптор приложения (экземпляр) нашей программы.

 

     HGLRC  hRC=NULL;              // Постоянный контекст рендеринга

     HDC  hDC=NULL;              // Приватный контекст устройства GDI

     HWND  hWnd=NULL;              // Здесь будет хранится дескриптор окна

     HINSTANCE  hInstance;              // Здесь будет хранится дескриптор приложения

 

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

 

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

 

Смысл переменной fullscreen вполне очевиден. Если наша программа запущена в полноэкранном режиме, fullscreen имеет значение true, если же она запущенна в оконном режиме, то fullscreen будет false. Очень важно сделать эти переменные глобальными, так чтобы все другие функции знали - приложение запущенно в полноэкранном режиме или нет.

 

     bool  keys[256];                // Массив, используемый для операций с клавиатурой

     bool  active=true;                // Флаг активности окна, установленный в true по умолчанию

     bool  fullscreen=true;              // Флаг режима окна, установленный в полноэкранный по умолчанию

 

Теперь мы должны описать прототип функции WndProc(). Причиной, по которой мы должны сделать это, является то, что функция CreateGLWindow() вызывает функцию WndProc(), но WndProc() описывается после CreateGLWindow(). А в языке C (как и в C++), для вызова функции необходимо ее определить или описать прототип этой функции. Именно так, мы и поступаем в следующей строке. И теперь CreateGLWindow() может вызывать WndProc().

 

     LRESULT  CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM );        // Прототип функции WndProc

 

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

 

          GLvoid ReSizeGLScene( GLsizei width, GLsizei height )        // Изменить размер и инициализировать окно GL

          {

            if( height == 0 )              // Предотвращение деления на ноль

            {

               height = 1;

            }

    

                 glViewport( 0, 0, width, height );          // Сброс текущей области вывода

 

Следующие строчки настраивают экран для перспективного вида. Предметы с увеличением расстояния становятся меньше. Это придаёт реалистичность сцене. Охват перспективы - 45 градусов, угол поворота оси рассчитывается на основе ширины и высоты окна. Значения 0.1f, 100.0f - отправная и конечная точки для того, чтобы определить какая будет глубина у экрана.

 

glMatrixMode(GL_PROJECTION) сообщает о том, что следующие две команды будут воздействовать на матрицу проекции. Матрица проекции отвечает за добавление в нашу сцену перспективного вида. glLoadIdentity() – это функция работает подобно сбросу. Она восстанавливает выбранную матрицу в первоначальное состояние. Раз матрица проекции сброшена, необходимо вычислить перспективу для сцены. После вызова glLoadIdentity() мы инициализируем перспективный вид нашей сцены. glMatrixMode(GL_MODELVIEW) сообщает, что любые новые трансформации будут воздействовать на матрицу вида модели. Матрица вида модели - то "место", где сохранена информация о наших объектах. Позже мы сбрасываем матрицу вида модели. Не волнуйтесь, если что-то из выше изложенного Вы не понимаете, я буду обучать всему этому в дальнейших уроках. Только запомните, что НАДО сделать, если Вы хотите получить красивую перспективную сцену.

 

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

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

 

    // Вычисление соотношения геометрических размеров для окна

    gluPerspective( 45.0f, (GLfloat)width/(GLfloat)height, 0.1f, 100.0f );

 

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

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

  }

 

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

 

       int InitGL( GLvoid )                // Все установки касаемо OpenGL происходят здесь

       {

 

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

 

    glShadeModel( GL_SMOOTH );            // Разрешить плавное цветовое сглаживание

 

Следующая строка устанавливает цвет, которым будет очищен экран. Для тех, кто не знает, как устроены цвета, я постараюсь кратко объяснять. Все значения могут быть в диапазоне от 0.0f до 1.0f, при этом 0.0 самый темный, а 1.0 самый светлый. Первый аргумент в glClearColor - это интенсивность Красного (Red), второй – Зеленного (Green), третий – Синего (Blue). Наибольшее значение – 1.0f, является самым ярким значением данного цвета. Последнее число - для Альфа (Прозрачность) значения. Когда начинается очистка экрана, я никогда не волнуюсь о четвертом числе. Пока оно будет 0.0f. Как его использовать, я объясню в другом уроке.

 

Вы можете получить различные цвета, смешивая три компоненты цвета. Поэтому, если Вы вызвали glClearColor(0.0f,0.0f,1.0f,0.0f) Вы произведете очистку экрана, с последующим закрашиванием его в ярко-синий цвет. Если Вы вызвали glClearColor(0.5f,0.0f,0.0f,0.0f) экран будет заполнен умеренно красным цветом. Не очень ярким (1.0f) и не темным (0.0f), а именно умеренно красным. Для того чтобы сделать белый фон, Вы должны установить все цвета в (1.0f). Черный - все компоненты цвета равны 0.0f.

 

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);          // Очистка экрана в черный цвет

 

Следующие три строки создают Буфер Глубины. Думайте о буфере глубины как о слоях на экране. Буфер глубины указывает, как далеко объекты находятся от экрана. Мы не будем реально использовать буфер глубины в этой программе, но любая программа с OpenGL, которая рисует на экране в 3D, будет его использовать. Он позволяет сортировать объекты для отрисовки, поэтому квадрат, расположенный под кругом не будет изображен поверх него. Буфер глубины очень важная часть OpenGL.

 

    glClearDepth( 1.0f );              // Разрешить очистку буфера глубины

    glEnable( GL_DEPTH_TEST );            // Разрешить тест глубины

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

 

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

 

    glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );      // Улучшение в вычислении перспективы

 

И, в конце концов, мы возвращаем true. Если мы хотим узнать, прошла ли инициализация как положено, мы должны проверить возвращаемое значение на true и false. Вы можете добавить код, который будет возвращать FALSE, если произошла какая-нибудь ошибка. Но сейчас это не должно Вас беспокоить.

 

    return true;                // Инициализация прошла успешно

  }

 

В следующей секции содержится весь код для рисования. Все, что Вы планируете для отрисовки на экране, будет содержатся в этой секции кода. В каждом уроке код будет добавляться в эту секцию программы. Если Вы уже понимаете OpenGL, Вы можете попробовать добавить в код простейшие формы на OpenGL, ниже вызова glLoadIdentity() и перед возвращением значения (return true). Если Вы новичок в OpenGL, подождите до следующего моего урока. Сейчас все что мы сделаем, это очистка экрана цветом, который мы определили выше, очистка буфера глубины и сброс сцены.

 

Возвращение значения true говорит нашей программе о том, что в этой секции не возникло никаких проблем. Если, по каким-либо причинам, Вы хотите остановить выполнение программы добавьте строчку return false, где-нибудь перед return true, таким образом мы сообщим программе, что в секции кода, выполняющего рисование, произошла какая-то ошибка. Тогда произойдёт выход из программы.

 

    int DrawGLScene( GLvoid )                // Здесь будет происходить вся прорисовка

    {

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

          glLoadIdentity();              // Сбросить текущую матрицу

          return true;                // Прорисовка прошла успешно

    }

 

Следующая секция кода вызывается только перед выходом из программы. Задача KillGLWindow() - освободить Контекст Рендеринга (hRC), Контекст Устройства (hDC) и, наконец, дескриптор окна (hWnd). Я добавил много проверок на наличие ошибок. Если программа неспособна удалить какую-нибудь часть из контекстов окна, появится окно (Message Box) с соответствующим сообщением об ошибке. Чем их больше будет создано (Message Box-ов), тем проще будет найти ошибку.

 

     GLvoid KillGLWindow( GLvoid )              // Корректное разрушение окна

     {

 

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

 

    if( fullscreen )              // Мы в полноэкранном режиме?

     {

 

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

 

              ChangeDisplaySettings( NULL, 0 );        // Если да, то переключаемся обратно в оконный режим

              ShowCursor( true );            // Показать курсор мышки

     }

 

Код приведённый ниже проверяет, существует ли Контекст Рендеринга (hRC). Если его нет, то программа переходит на секцию кода, расположенную ниже и проверяющие существует ли Контекст Устройства (hDC).

 

            if( hRC )                // Существует ли Контекст Рендеринга?

             {

 

Если он существует, код ниже проверит, возможно ли освободить его (отсоединить RC от DC). С помощью сообщений можно отследить ошибки. Мы просим программу освободить Контекст Рендеринга (с помощью вызова wglMakeCurrent(NULL,NULL)), затем проверяем, благополучно ли завершена эта операция или нет.

 

      if( !wglMakeCurrent( NULL, NULL ) )        // Возможно ли освободить RC и DC?

       {

 

Если невозможно уничтожить контексты RC и DC, выскочит сообщение об ошибке, это позволит понять, что контексты не уничтожены. NULL в функции MessageBox() означает, что у сообщения не будет родительского окна. Текст справа от NULL - текст, который будет содержать сообщение. "SHUTDOWN ERROR" - текст, который будет содержаться в заголовке окна-сообщения. Следом мы видим MB_OK, это означает, что окно-сообщение будет с одной кнопкой помеченной "ОК". MB_ICONINFORMATION создаёт иконку в области окна-сообщения (что заставляет обратить на себя внимание).

 

                  MessageBox( NULL, "Release Of DC And RC Failed.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION );

       }

 

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

 

      if( !wglDeleteContext( hRC ) )        // Возможно ли удалить RC?

       {

 

Если невозможно удалить Контекст Рендеринга код, приведённый ниже, выведет окно-сообщение позволяющее понять, что его удаление - невозможно. hRC будет установлено в NULL.

 

          MessageBox( NULL, "Release Rendering Context Failed.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION );

       }

      hRC = NULL;              // Установить RC в NULL

       }

 

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

 

    if( hDC && !ReleaseDC( hWnd, hDC ) )          // Возможно ли уничтожить DC?

     {

        MessageBox( NULL, "Release Device Context Failed.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION );

        hDC=NULL;                // Установить DC в NULL

     }

 

Теперь проверим, есть ли дескриптор окна, а если есть, мы попробуем уничтожить окно, используя DestroyWindow(hWnd). Если это невозможно окно-сообщение выведет соответствующее сообщение и hWnd будет установлен в NULL.

 

  if(hWnd && !DestroyWindow(hWnd))            // Возможно ли уничтожить окно?

  {

    MessageBox( NULL, "Could Not Release hWnd.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION );

    hWnd = NULL;                // Установить hWnd в NULL

  }

 

Последнее, что нам необходимо сделать так это разрегистрировать (операция, обратная регистрации) класс окна. Это позволяет нам корректным образом уничтожить окно и открыть другое без получения сообщения об ошибке "Windows Class already registered" (Класс окна уже зарегистрирован).

 

  if( !UnregisterClass( "OpenGL", hInstance ) )        // Возможно ли разрегистрировать класс

  {

    MessageBox( NULL, "Could Not Unregister Class.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION);

    hInstance = NULL;                // Установить hInstance в NULL

  }

}

 

Следующая секция кода создаёт наше OpenGL окно. Я провёл много времени, пытаясь решить, создавать ли полноэкранное приложение, что требует немного кода. Или же позволить, выбирать удобный для пользователя режим окна, что потребует больше кода. Я решил, что удобный для пользователя режим окна с большим количеством кода лучший выбор. Я всё время получал такие вопросы по электронной почте, как: "Как я могу создать окно, вместо того, чтобы использовать полноэкранный режим?", "Как можно изменить заголовок окна?", "Как можно изменить разрешение экрана или формат пикселей окна?". Следующий далее код решает все эти вопросы. Поэтому лучше понять этот материал, и тогда написание программ на OpenGL покажется гораздо легче!

 

Как Вы можете видеть, функция возвращает BOOL (true или false), всего она получает 5 аргументов: Заголовок Окна, Ширину Окна, Высоту Окна, Число Битов (16/24/32), и, наконец, флаг режима (true для полноэкранного или false для оконного). Мы возвращаем логическую переменную, которая говорит о том, возможно ли создать окно.

 

BOOL CreateGLWindow( LPCWSTR title, int width, int height, int bits, bool fullscreenflag )

{

 

Когда мы просим Windows найти формат пикселей, соответствующий тому, который мы хотим, номер режима, который нашёл Windows, будет храниться в переменной PixelFormat.

 

  GLuint    PixelFormat;              // Хранит результат после поиска

 

Переменная wc будет использоваться для хранения структуры класса нашего окна. Структура класса содержит информацию о нашем окне. Изменяя различные поля класса, мы можем изменить вид окна и его поведение. Каждому окну соответствует определённый класс. Перед созданием окна Вы должны ЗАРЕГИСТРИРОВАТЬ класс для окна.

 

  WNDCLASS  wc;                // Структура класса окна

 

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

 

  DWORD    dwExStyle;              // Расширенный стиль окна

  DWORD    dwStyle;              // Обычный стиль окна

 

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

 

  RECT WindowRect;                // Grabs Rectangle Upper Left / Lower Right Values

  WindowRect.left=(long)0;              // Установить левую составляющую в 0

  WindowRect.right=(long)width;              // Установить правую составляющую в Width

  WindowRect.top=(long)0;                // Установить верхнюю составляющую в 0

  WindowRect.bottom=(long)height;              // Установить нижнюю составляющую в Height

 

В следующей строчке кода мы присваиваем глобальной переменной fullscreen значение переменной fullscreenflag.

 

  fullscreen=fullscreenflag;              // Устанавливаем значение глобальной переменной fullscreen

 

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

 

Стили CS_HREDRAW и CS_VREDRAW принуждают перерисовать окно всякий раз, когда оно перемещается. CS_OWNDC создает скрытый DC для окна. Это означает, что DC не используется совместно несколькими приложениями. WndProc - процедура, которая перехватывает сообщения для программы. Дополнительной информации для нашего окна нет, поэтому заполняем два этих (cbClsExtra и cbWndExtra) поля нулями. Затем мы устанавливаем дескриптор приложения. hIcon установлен равным нулю, это означает, что мы не хотим ICON в окне, и для мыши используем стандартный указатель. Фоновый цвет не имеет значения (мы установим его в GL). Мы не хотим меню в этом окне, поэтому мы используем установку его в NULL, и имя класса – это любое имя которое Вы хотите. Для простоты я использую "OpenGL".

 

  hInstance    = GetModuleHandle(NULL);        // Считаем дескриптор нашего приложения

  wc.style    = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;      // Перерисуем при перемещении и создаём скрытый DC

  wc.lpfnWndProc    = (WNDPROC) WndProc;          // Процедура обработки сообщений

  wc.cbClsExtra    = 0;              // Нет дополнительной информации для окна

  wc.cbWndExtra    = 0;              // Нет дополнительной информации для окна

  wc.hInstance    = hInstance;            // Устанавливаем дескриптор

  wc.hIcon    = LoadIcon(NULL, IDI_WINLOGO);        // Загружаем иконку по умолчанию

  wc.hCursor    = LoadCursor(NULL, IDC_ARROW);        // Загружаем указатель мышки

  wc.hbrBackground  = NULL;              // Фон не требуется для GL

  wc.lpszMenuName    = NULL;              // Меню в окне не будет

  wc.lpszClassName  = "OpenGL";            // Устанавливаем имя классу

 

Теперь надо зарегистрировать класс. Если возникнет какая-либо проблема или ошибка, выскочит соответствующее окно-сообщение. Кликнув "ОК" в нём мы выйдем из программы.

 

  if( !RegisterClass( &wc ) )              // Пытаемся зарегистрировать класс окна

  {

    MessageBox( NULL, "Failed To Register The Window Class.", "ERROR", MB_OK | MB_ICONEXCLAMATION );

    return false;                // Выход и возвращение функцией значения false

  }

 

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

 

  if( fullscreen )                // Полноэкранный режим?

  {

 

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

 

    DEVMODE dmScreenSettings;            // Режим устройства

    memset( &dmScreenSettings, 0, sizeof( dmScreenSettings ) );    // Очистка для хранения установок

    dmScreenSettings.dmSize=sizeof( dmScreenSettings );      // Размер структуры Devmode

    dmScreenSettings.dmPelsWidth  =   width;        // Ширина экрана

    dmScreenSettings.dmPelsHeight  =   height;        // Высота экрана

    dmScreenSettings.dmBitsPerPel  =   bits;        // Глубина цвета

    dmScreenSettings.dmFields= DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;// Режим Пикселя

 

В коде, приведённом выше, мы очищаем память для хранения наших видео настроек. Мы устанавливаем ширину, высоту и глубину цвета, которые мы хотим иметь. В коде ниже мы пытаемся подать запрос на установление полноэкранного режима. Вся информация о ширине, высоте и глубине цвета хранится в dmScreenSettings. В строчке ниже функция ChangeDisplaySettings пробует переключить экран в режим, настройки которого хранятся в dmScreenSettings. Я использую параметр CDS_FULLSCREEN, когда переключаю режим потому, что он позволяет скрыть панель управления Windows (в том числе кнопку "Пуск") внизу экрана, к тому же это позволяет нам избежать перемещения и изменения размеров рабочего стола, когда мы переключаемся в полноэкранный режим и обратно.

 

    // Пытаемся установить выбранный режим и получить результат.  Примечание: CDS_FULLSCREEN убирает панель управления.

    if( ChangeDisplaySettings( &dmScreenSettings, CDS_FULLSCREEN ) != DISP_CHANGE_SUCCESSFUL )

    {

 

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

 

      // Если переключение в полноэкранный режим невозможно, будет предложено два варианта: оконный режим или выход.

      if( MessageBox( NULL, "The Requested Fullscreen Mode Is Not Supported By\nYour Video Card. Use Windowed Mode Instead?",

          "NeHe GL", MB_YESNO | MB_ICONEXCLAMATION) == IDYES )

      {

 

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

 

        fullscreen = false;          // Выбор оконного режима (fullscreen = false)

      }

      else

      {

 

Если пользователь решил выйти из приложения выскочит окно с сообщением о закрытие программы. Функция вернёт false, что сообщит нашей программе, что создание окна не завершилось успехом. Затем мы выйдем.

 

        // Выскакивающее окно, сообщающее пользователю о закрытие окна.

        MessageBox( NULL, "Program Will Now Close.", "ERROR", MB_OK | MB_ICONSTOP );

        return false;            // Выход и возвращение функцией false

      }

    }

  }

 

Поскольку код выше мог вызвать ошибку инициализации полноэкранного режима, и пользователь мог решить запускать программу всё же в оконном режиме, мы проверяем ещё раз значение переменной fullscreen (true или false) перед тем, как мы установим режим экрана/окна.

 

  if(fullscreen)                  // Мы остались в полноэкранном режиме?

  {

 

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

 

И, наконец, мы запретим использования указателя мышки. Если программа не является интерактивной, правилом хорошего тона является сокрытия указателя мышки в полноэкранном режиме. Но это решать Вам.

 

    dwExStyle  =   WS_EX_APPWINDOW;          // Расширенный стиль окна

    dwStyle    =   WS_POPUP;            // Обычный стиль окна

    ShowCursor( false );              // Скрыть указатель мышки

  }

  else

  {

 

Если мы используем оконный режим, вместо полноэкранного режима, мы добавим WS_EX_WINDOWEDGE в расширенный стиль. Это придаст окну более объёмный вид. Для обычного стиля зададим параметр WS_OVERLAPPEDWINDOW вместо WS_POPUP. WS_OVERLAPPEDWINDOW создаёт окно с заголовком, границы для регулировки размера, оконное меню и кнопки для сворачивания/разворачивания.

 

    dwExStyle  =   WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;      // Расширенный стиль окна

    dwStyle    =   WS_OVERLAPPEDWINDOW;        // Обычный стиль окна

  }

 

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

 

  AdjustWindowRectEx( &WindowRect, dwStyle, false, dwExStyle );      // Подбирает окну подходящие размеры

 

В следующей секции кода мы приступим к созданию нашего окна и проверке создано ли оно должным образом. Мы передаём CreateWindowEx() все параметры, в которых она нуждается:

 

·       определённый ранее расширенный стиль;

·       имя класса (которое должно быть тем самым, что Вы использовали, когда регистрировали класс окна);

·       заголовок окна;

·       обычный стиль окна;

·       X левого угла окна;

·       Y левого угла окна;

·       ширина окна;

·       высота окна;

·       родительское окно (у нас его нет);

·       дескриптор меню (и меню у нас тоже нет);

·       дескриптор приложения;

·       дополнительные данные.

 

Прошу обратить внимание: мы подключаем дополнительные стили WS_CLIPSIBLINGS и WS_CLIPCHILDREN вместе с теми стилями, которые мы уже решили использовать. WS_CLIPSIBLINGS и WS_CLIPCHILDREN требуются для работы OpenGL соответствующим образом. Эти стили не допускают другим окнам рисоваться поверх или внутри нашего OpenGL окна.

 

  if( !( hWnd = CreateWindowEx(  dwExStyle,          // Расширенный стиль для окна

          _T("OpenGL"),          // Имя класса

          title,            // Заголовок окна

          WS_CLIPSIBLINGS |        // Требуемый стиль для окна

          WS_CLIPCHILDREN |        // Требуемый стиль для окна

          dwStyle,          // Выбираемые стили для окна

          0, 0,            // Позиция окна

          WindowRect.right-WindowRect.left,    // Вычисление подходящей ширины

          WindowRect.bottom-WindowRect.top,    // Вычисление подходящей высоты

          NULL,            // Нет родительского

          NULL,            // Нет меню

          hInstance,          // Дескриптор приложения

          NULL ) ) )          // Не передаём ничего до WM_CREATE (???)

 

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

 

  {

    KillGLWindow();                // Восстановить экран

    MessageBox( NULL, "Window Creation Error.", "ERROR", MB_OK | MB_ICONEXCLAMATION );

    return false;                // Вернуть false

  }

 

Следующая секция кода описывает Формат Пикселей (Pixel Format). Мы выбираем формат, который поддерживает OpenGL и двойной буфер, в соответствии с RGBA (Red-красный, Green-зелёный, Blue-синий, Alpha Channel-альфа канал (канал прозрачности)). Мы пытаемся найти формат пикселя, который соответствует такому количеству бит, выделенных на глубину цвета, какое заранее будет заданно. И, наконец, мы установим 32 битный Z-буфер (буфер глубины). Остальные параметры или не используются, или не важны (кроме буфера трафарета и (медленного) буфера накопления).

 

  static  PIXELFORMATDESCRIPTOR pfd=            // pfd сообщает Windows каким будет вывод на экран каждого пикселя

  {

    sizeof(PIXELFORMATDESCRIPTOR),            // Размер дескриптора данного формата пикселей

    1,                  // Номер версии

    PFD_DRAW_TO_WINDOW |              // Формат для Окна

    PFD_SUPPORT_OPENGL |              // Формат для OpenGL

    PFD_DOUBLEBUFFER,              // Формат для двойного буфера

    PFD_TYPE_RGBA,                // Требуется RGBA формат

    bits,                  // Выбирается бит глубины цвета

    0, 0, 0, 0, 0, 0,              // Игнорирование цветовых битов

    0,                  // Нет буфера прозрачности

    0,                  // Сдвиговый бит игнорируется

    0,                  // Нет буфера накопления

    0, 0, 0, 0,                // Биты накопления игнорируются

    32,                  // 32 битный Z-буфер (буфер глубины)

    0,                  // Нет буфера трафарета

    0,                  // Нет вспомогательных буферов

    PFD_MAIN_PLANE,                // Главный слой рисования

    0,                  // Зарезервировано

    0, 0, 0                  // Маски слоя игнорируются

  };

 

Если во время создания окна не возникло ни одной ошибки, мы попытаемся получить Контекст Устройства (DC) OpenGL. Если мы не можем получить DC, на экран выскочит сообщение об ошибке и программа завершит работу (функция возвратит false).

 

  if( !( hDC = GetDC( hWnd ) ) )              // Можем ли мы получить Контекст Устройства?

  {

    KillGLWindow();                // Восстановить экран

    MessageBox( NULL, "Can't Create A GL Device Context.", "ERROR", MB_OK | MB_ICONEXCLAMATION );

    return false;                // Вернуть false

  }

 

Если мы смогли получить Контекст Устройства для нашего OpenGL окна мы попробуем найти формат пикселя, который мы описали выше. Если Windows не может найти подходящий формат, на экран выскочит сообщение об ошибке и программа завершит работу (функция возвратит false).

 

  if( !( PixelFormat = ChoosePixelFormat( hDC, &pfd ) ) )        // Найден ли подходящий формат пикселя?

  {

    KillGLWindow();                // Восстановить экран

    MessageBox( NULL, "Can't Find A Suitable PixelFormat.", "ERROR", MB_OK | MB_ICONEXCLAMATION );

    return false;                // Вернуть false

  }

 

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

 

  if( !SetPixelFormat( hDC, PixelFormat, &pfd ) )          // Возможно ли установить Формат Пикселя?

  {

    KillGLWindow();                // Восстановить экран

    MessageBox( NULL, "Can't Set The PixelFormat.", "ERROR", MB_OK | MB_ICONEXCLAMATION );

    return false;                // Вернуть false

  }

 

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

 

  if( !( hRC = wglCreateContext( hDC ) ) )          // Возможно ли установить Контекст Рендеринга?

  {

    KillGLWindow();                // Восстановить экран

    MessageBox( NULL, "Can't Create A GL Rendering Context.", "ERROR", MB_OK | MB_ICONEXCLAMATION);

    return false;                // Вернуть false

  }

 

Если у нас до сих пор не возникало ошибок, т. е. мы смогли создать и Контекст Устройства и Контекст Рендеринга, всё, что мы должны сделать теперь - сделать Контекст Рендеринга активным. Если у нас этого не получится, на экран выскочит сообщение об ошибке и программа завершит работу (функция возвратит false).

 

  if( !wglMakeCurrent( hDC, hRC ) )            // Попробовать активировать Контекст Рендеринга

  {

    KillGLWindow();                // Восстановить экран

    MessageBox( NULL, "Can't Activate The GL Rendering Context.", "ERROR", MB_OK | MB_ICONEXCLAMATION );

    return false;                // Вернуть false

  }

 

Если всё прошло гладко, и наше OpenGL окно было создано, мы покажем окно, установим его на передний план (присвоив более высокий приоритет) и затем установим фокус для этого окна. Потом мы вызовем ReSizeGLScene, передавая ширину и высоту экрана для настройки перспективы для нашего OpenGL экрана.

 

  ShowWindow( hWnd, SW_SHOW );              // Показать окно

  SetForegroundWindow( hWnd );              // Слегка повысим приоритет

  SetFocus( hWnd );                // Установить фокус клавиатуры на наше окно

  ReSizeGLScene( width, height );              // Настроим перспективу для нашего OpenGL экрана.

 

Наконец мы переходим к выполнению функции InitGL(), где мы можем настроить освещение, текстуры и что-нибудь еще, если это необходимо. Вы можете сделать дополнительные проверки на ошибки в InitGL(), и передать true (всё OK) или false (что-то не так). Например: если Вы загружаете текстуры в InitGL() и возникают ошибки, Вы можете захотеть, чтобы программа остановилась. Если InitGL() возвращает главной функции false, строчки кода ниже среагируют на это сообщением об ошибке и программа завершится.

 

  if( !InitGL() )                  // Инициализация только что созданного окна

  {

    KillGLWindow();                // Восстановить экран

    MessageBox( NULL, _T("Initialization Failed."), _T("ERROR"), MB_OK | MB_ICONEXCLAMATION );

    return false;                // Вернуть false

  }

 

Если программа дошла до этого момента, логично предположить, что создание окна закончилось успехом. Мы возвращаем true в WinMain(), что сообщает о том, что не возникло никаких ошибок. То есть мы не выходим из программы, а благополучно продолжаем работу.

 

  return true;                  // Всё в порядке!

}

 

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

 

LRESULT CALLBACK WndProcHWND  hWnd,            // Дескриптор нужного окна

        UINT  uMsg,            // Сообщение для этого окна

        WPARAM  wParam,            // Дополнительная информация

        LPARAM  lParam)            // Дополнительная информация

{

 

Код ниже устанавливает uMsg как параметр, с которым мы будем сравнивать все блоки. uMsg будет хранить название сообщения, с которым мы будем работать.

 

  switch (uMsg)                // Проверка сообщения для окна

  {

 

Если uMsg - WM_ACTIVATE проверяем, активно ли еще наше окно. Если оно минимизировано, переменная active будет равна false. Если окно активно, переменная active будет равна true.

 

    case WM_ACTIVATE:            // Проверка сообщения активности окна

    {

      if( !HIWORD( wParam ) )          // Проверить состояние минимизации

      {

        active = true;          // Программа активна

      }

      else

      {

        active = false;          // Программа теперь не активна

      }

 

      return 0;            // Возвращаемся в цикл обработки сообщений

    }

 

Если возникло сообщение WM_SYSCOMMAND (системная команда) мы будем сравнивать состояния по параметру wParam. Если wParam - SC_SCREENSAVE или SC_MONITORPOWER, то или запускается скринсейвер (программа сохранения экрана) или монитор пытается перейти в режим сбережения энергии. Возвращая ноль мы предотвращаем наступлению обоих этих событий.

 

    case WM_SYSCOMMAND:            // Перехватываем системную команду

    {

      switch ( wParam )            // Останавливаем системный вызов

      {

        case SC_SCREENSAVE:        // Пытается ли запустится скринсейвер?

        case SC_MONITORPOWER:        // Пытается ли монитор перейти в режим сбережения энергии?

        return 0;          // Предотвращаем это

      }

      break;              // Выход

    }

 

Если uMsg - WM_CLOSE окно будет закрыто. Мы отправляем сообщение о выходе, чтобы прервать выполнение главного цикла. Переменная done будет установлена в true, главный цикл в WinMain() будет остановлен и программа будет закрыта.

 

    case WM_CLOSE:              // Мы получили сообщение о закрытие?

    {

      PostQuitMessage( 0 );          // Отправить сообщение о выходе

      return 0;            // Вернуться назад

    }

 

Если произошло нажатие кнопки (на клавиатуре) мы можем узнать какая клавиша это была считав wParam. Тогда я делаю, чтобы эта ячейка в массиве keys[ ] содержала true. Таким образом, я могу считать этот массив позже и найти какая клавиша была нажата. Это позволяет отследить нажатия сразу несколько клавиш одновременно.

 

    case WM_KEYDOWN:            // Была ли нажата кнопка?

    {

      keys[wParam] = true;          // Если так, мы присваиваем этой ячейке true

      return 0;            // Возвращаемся

    }

 

Если кнопка была отпущена мы можем узнать какая клавиша это была считав wParam. Тогда мы делаем, чтобы эта ячейка в массиве keys[ ] была равна false. Таким образом, когда Вы считываете эту ячейку, Вы будете знать нажата ли она до сих пор или была отпущена. Другие кнопки на клавиатуре могут быть представлены в диапазоне 0-255. Когда мы нажимаем кнопку со скан-кодом 40, например, keys[40] вернёт true. Когда я её отпущу, она вернёт false. Вот так мы используем ячейки для хранения нажатых клавиш.

 

    case WM_KEYUP:              // Была ли отпущена клавиша?

    {

      keys[wParam] = false;          //  Если так, мы присваиваем этой ячейке false

      return 0;            // Возвращаемся

    }

 

Всякий раз, когда изменяются размеры нашего окна uMsg в конечном счёте будет иметь значение WM_SIZE. Мы считываем LOWORD и HIWORD (младшее и старшее слова) переменной lParam для того, чтобы узнать новые высоту и ширину окна. Мы передаём эти аргументы функции ReSizeGLScene(). OpenGL сцена перерисуется с новой шириной и высотой.

 

    case WM_SIZE:              // Изменены размеры OpenGL окна

    {

      ReSizeGLScene( LOWORD(lParam), HIWORD(lParam) );  // Младшее слово=Width, старшее слово=Height

      return 0;            // Возвращаемся

    }

  }

 

Любое сообщение, которое мы не проверили, будет передано в качестве фактического параметра функции DefWindowProc для того, чтобы Windows могла его обработать.

 

  // пересылаем все необработанные сообщения DefWindowProc

  return DefWindowProc( hWnd, uMsg, wParam, lParam );

}

 

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

 

int WINAPI WinMain(  HINSTANCE  hInstance,        // Дескриптор приложения

      HINSTANCE  hPrevInstance,        // Дескриптор родительского приложения

      LPSTR    lpCmdLine,        // Параметры командной строки

      int    nCmdShow )        // Состояние отображения окна

{

 

Мы инициализируем две переменные. Переменная msg будет использоваться для того, чтобы проверить существует ли какое-нибудь ожидающее обработки сообщение. Переменная done при старте будет равна false. Это означает, что наша программа не закончила своего выполнения. Пока done равно false программа будет продолжать выполнение. Как только done изменит значение с false на true - наша программа закончит выполнение.

 

  MSG  msg;              // Структура для хранения сообщения Windows

  BOOL  done = false;            // Логическая переменная для выхода из цикла

 

Эта секция кода является полностью дополнительной. Она вызовет всплывающее окно, которое спросит, хотите ли Вы запустить приложение в полноэкранном режиме. Если пользователь кликнет на кнопке "NO", переменная изменится с true (по умолчанию) на false и программа запустится в оконном режиме.

 

  // Спрашивает пользователя, какой режим экрана он предпочитает

  if( MessageBox( NULL, "Хотите ли Вы запустить приложение в полноэкранном режиме?",  "Запустить в полноэкранном режиме?", MB_YESNO | MB_ICONQUESTION) == IDNO )

  {

    fullscreen = false;          // Оконный режим

  }

 

Тут мы задаем, как будет создано окно. Мы передаём заголовок, ширину, высоту, глубину цвета и true (полноэкранный режим) или false (оконный режим) функции CreateGLWindow. Вот так! Я вполне доволен такой простотой кода. Если окно не будет создано по какой бы то ни было причине, CreateGLWindow вернёт false и наша программа немедленно завершиться (return 0).

 

  // Создать наше OpenGL окно

  if( !CreateGLWindow( "NeHe OpenGL окно", 1024, 768, 32, fullscreen ) )

  {

    return 0;              // Выйти, если окно не может быть создано

  }

 

Здесь стартует наш цикл. Пока done равно false цикл будет повторяться.

 

  while( !done )                // Цикл продолжается, пока done не равно true

  {

 

Первым делом мы должны проверить стоит ли в очереди какое-нибудь сообщение. Используя PeekMessage() мы можем сделать это без остановки выполнения нашей программы. Многие программы используют GetMessage(). Это работает не плохо, но при вызове GetMessage() Ваша программа ничего не делает, пока не получит сообщение о перерисовке или ещё какое-либо другое сообщение.

 

    if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )    // Есть ли в очереди какое-нибудь сообщение?

    {

 

В следующей секции кода мы проверяем, есть ли в очереди сообщение о выходе. Если текущее сообщение WM_QUIT, это повод для вызова PostQuitMessage( 0 ), установки переменной done в true и завершения программы.

 

      if( msg.message == WM_QUIT )        // Мы поучили сообщение о выходе?

      {

        done = true;          // Если так, done=true

      }

      else              // Если нет, обрабатывает сообщения

      {

 

Если сообщение в очереди не сообщение о выходе, мы преобразуем сообщение, затем отсылаем его так, чтобы WndProc() или Windows могли работать с ним.

 

        TranslateMessage( &msg );        // Переводим сообщение

        DispatchMessage( &msg );        // Отсылаем сообщение

      }

    }

    else                // Если нет сообщений

    {

 

Если не было сообщений, мы рисуем нашу сцену. Первая строчка кода ниже проверяет активно ли окно. Если нажата кнопка ESC, переменная done устанавливается в true, приводя к выходу из программы.

 

      // Прорисовываем сцену.

      if( active )          // Активна ли программа?

      {

        if(keys[VK_ESCAPE])        // Было ли нажата клавиша ESC?

        {

          done = true;      // ESC говорит об останове выполнения программы

        }

        else            // Не время для выхода, обновим экран.

        {

 

Если программа активна и не нажата ESC мы визуализируем сцену и меняем буфер (используя двойную буферизацию мы исключаем мерцание при анимации). Используя двойную буферизацию мы рисуем всё на "скрытом экране" (второй буфер) так, чтобы мы не могли видеть этого. Когда мы меняем буфер, экран (первый буфер), который мы видим, становится скрытым, а скрытый - становится видимым. Таким образом, мы не видим саму прорисовку сцены, а только результат визуализации.

 

          DrawGLScene();        // Рисуем сцену

          SwapBuffers( hDC );    // Меняем буфер (двойная буферизация)

        }

      }

 

Следующий код позволяет нам менять полноэкранный режим на оконный и обратно, нажимая клавишу F1.

 

      if( keys[VK_F1] )          // Была ли нажата F1?

      {

        keys[VK_F1] = false;        // Если так, меняем значение ячейки массива на false

        KillGLWindow();          // Разрушаем текущее окно

        fullscreen = !fullscreen;      // Переключаем режим

        // Пересоздаём наше OpenGL окно

        if( !CreateGLWindow( _T("NeHe OpenGL структура"), 1024, 768, 32, fullscreen ) )

        {

          return 0;        // Выходим, если это невозможно

        }

      }

    }

  }

 

Если переменная done больше не false, программа завершается. Мы корректно разрушаем наше OpenGL окно, чтобы всё было освобождено и выходим из программы.

 

  // Shutdown

  KillGLWindow();                // Разрушаем окно

  return ( msg.wParam );              // Выходим из программы

}

 

В этом уроке я попытался объяснить как можно больше деталей каждого шага запутанной установки, и создания ваших собственных полноэкранных OpenGL программ, которые будут завершаться при нажатии ESC и контролировать активно ли окно или нет. Я потратил две недели на написание кода, неделю на исправление ошибок и общаясь с программистскими гуру, и еще два дня (около 22 часов на написания этого HTML файла). Если у Вас есть любые комментарии или вопросы, пожалуйста, пошлите их мне по электронной почте. Если Вы поняли, что я некорректно прокомментировал что-то или что код должен быть лучше в некоторых секциях по некоторым причинам, пожалуйста, дайте мне знать. Я хочу сделать уроки по OpenGL хорошими насколько смогу. Я заинтересован в обратной связи.

© Jeff Molofee (NeHe)

PMG  8 февраля 2007 (c)  Евгений Захарцев