22 материала для изучения OpenGL


Оглавление (нажмите, чтобы открыть):

C# + OpenGL — работа с материалами

Основы работы с освещением в OpenGL

В графическом программирование материалы занимают достаточно важную роль. Материал может рассеивать, отражать и излучать свет.

Свойства материала можно изменять при помощи специальной функции:

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

Второй параметр определяет свойство материала, которое будет установлено, и может принимать следующие значения:

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

Третий параметр определяет цвет в виде массива. В случае Gl.GL_SHININESS указывает на число типа float, которое должно быть в диапазоне от до 128.

Изобразим чайник красного цвета, отражающий свет:

Наш чайник готов.

Рисунок 1. Чайник с заданными параметрами и свойствами.

Уроки OpenGL

Урок 1. Введение

Что нужно знать

Забудьте все, что знали про OpenGL 1/2

Сборка проекта

  1. Обновите драйвера на вашу видеокарту!! Я вас предупредил:)
  2. Скачайте компилятор, если у вас его еще нет.
  3. Установите CMake
  4. Скачайте готовые исходники уроков.
  5. Сгенерируйте проект с помощью CMake
  6. Соберите проект.
  7. Поэкспериментируйте с кодом для лучшего понимания, что там происходит.

Сборка под Windows

Сборка под Linux

  1. Установите последние драйвера на вашу видеокарту. Очень рекомендую не опенсорсные драйвера. Они не входят в состав GNU, но они часто работают гораздо лучше. Если ваша сборка линукса не предоставляет автоматического инсталлятора, попробуйте почитать Ubuntu’s gu > sudo apt-get install ***** или su / yum install ******
  2. Скачайте исходники примеров и разархивируйте их в папку, например,

/Projects/OpenGLTutorials/
Зайдите в папку

/ Projects / OpenGLTutorials / и введите следующие команды:

  • mkdir build
  • cd build
  • cmake ..
  1. Если предыдущие команды были выполнены успешно, то в папке build/ будет создан makefile
  2. введите «make all» и после этого будут скомпилированы все примеры и их зависимости. Если не будет никаких ошибок, то готовые исполняемые файлы будут помещены в папку

Инструкция по сборке проекта в QtCreator:

/ opengl — tutorial /tutorial02_red_triangle/

Запуск примеров

Как проходить эти уроки

Открываем окно

19 комментариев:

подскажите, пожалуйста, где найти исходники??

Исходники можно скачать с оригинального сайта:
http://www.opengl-tutorial.org/download/

Спасибо за проделанную работу!
Но есть вопрос.
Я скомпилировал уроки и они работают.
Но мне б хотелось создать проект с нуля в vs2012
как я делаю Файл-создать-проект-с++-пустое приложение-добавляю файл main.cpp в него копирую код первого урока библиотеки он находит подгружает зависимости.
но скомпилировать не удается
main.obj : error LNK2020: ссылка на неразрешенный внешний символ __imp__glClearColor@16 в функции _main
1>main.obj : error LNK2020: ссылка на неразрешенный внешний символ __imp__glewInit@0 в функции _main
1>main.obj : error LNK2020: ссылка на неразрешенный внешний символ _glfwInit в функции _main
1>main.obj : error LNK2020: ссылка на неразрешенный внешний символ _glfwTerminate в функции _main
1>main.obj : error LNK2020: ссылка на неразрешенный внешний символ _glfwOpenWindow в функции _main
1>main.obj : error LNK2020: ссылка на неразрешенный внешний символ _glfwOpenWindowHint в функции _main
1>main.obj : error LNK2020: ссылка на неразрешенный внешний символ _glfwSetWindowTitle в функции _main
1>main.obj : error LNK2020: ссылка на неразрешенный внешний символ _glfwSwapBuffers в функции _main
1>main.obj : error LNK2020: ссылка на неразрешенный внешний символ _glfwGetWindowParam в функции _main
1>main.obj : error LNK2020: ссылка на неразрешенный внешний символ _glfwGetKey в функции _main
1>main.obj : error LNK2020: ссылка на неразрешенный внешний символ _glfwEnable в функции _main
выдает такие ошибки
посмотрев свойства проекта 1 урока
добавлял в свой зависимости в линкер либы и что еще только не делал
помогите пожалуйста

Можно попробовать добавить где-нибудь вначалеЖ
#pragma comment(lib, «opengl32.lib»)

OpenGL и материалы

20.04.2012, 16:18

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

Указать материалы, направление
Доброго времени суток, прошу у Вас помощи. Мне нужна программа, которая взаимодействует с.

Материалы для изучения
Подскажите пожалуйста какие-нибудь статьи по ссылкам и указателям.

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

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

Уроки по OpenGL с сайта OGLDev

Исходные коды для всех уроков можно получить по ссылке: Git репозиторий

Урок 01 — Создание окна

Урок 02 — Привет, точка!

Урок 03 — Первый треугольник

Урок 04 — Шейдеры

Урок 05 — Uniform переменные

Урок 06 — Перемещение

Урок 07 — Вращение

Урок 08 — Преобразования масштаба

Урок 09 — Интерполяция

Урок 10 — Индексированная отрисовка

Урок 11 — Объединение преобразований

Урок 12 — Проекция перспективы

Урок 13 — Пространство камеры

Урок 14 — Управление камерой — часть 1

Урок 15 — Управление камерой — часть 2

Урок 16 — Основы наложения текстур

Урок 17 — Фоновое освещение

Урок 18 — Рассеянное освещение

Урок 19 — Отраженный свет

Урок 20 — Точечный источник света

Урок 21 — Прожектор

Урок 22 — Загрузка моделей через Assimp

Урок 23 — Карта теней — часть 1

Урок 24 — Карта теней — часть 2

Урок 25 — Скайбокс

Урок 26 — Карта нормалей

Урок 27 — Billboarding и Геометрический шейдер

Урок 28 — Система частиц и Transform Feedback

Урок 29 — 3D Выбор

Урок 30 — Основы Тесселяции

Урок 31 — Тесселяция PN треугольников

Урок 32 — Vertex Array Objects

Урок 33 — Дублирующий рендер (Instanced Rendering)

Урок 34 — GLFX — An OpenGL Effect Library

Урок 35 — Deferred Shading — Часть 1

Урок 36 — Deferred Shading — Часть 2

Урок 37 — Deferred Shading — Часть 3

Урок 38 — Скелетная анимация с Assimp

Урок 39 — Обнаружение силуэта

Урок 40 — Теневой объем (Stencil Shadow Volume)

Урок 41 — Размытие (Motion Blur)

Урок 42 — Percentage Closer Filtering

Урок 43 — Многопроходные карты теней с точечным источником света

Урок 45 — Screen Space Ambient Occlusion

Урок 46 — SSAO С Востановлением Глубины

Урок 47 — Карты теней с направленным источником света

Урок 48 — Пользовательский интерфейс с Ant Tweak Bar

Опыт изучения OpenGL — Часть 1 — Введение

С детства мечтал писать компьютерные игры, и вот, года три назад решил рискнуть — поизучать OpenGL. Почему взялся изучать не игровой движок, а низкоуровневый API (API — Application Programming Interface)? Потому что люблю изучать все с как можно более низкого уровня, чтобы разобраться как оно работает (например, изучение языка ассемблера Intel очень помогло моему пониманию языков Си и C++). А почему OpenGL, а не DirectX? — Просто по OpenGL я смог найти больше литературы, чем по DirectX. Изучать OpenGL — задачка очень непростая, а теперь появился еще более низкоуровневый API — Vulkan, пытаюсь и про него читать понемногу. Но сначала хочу поделиться с вами своим опытом изучения OpenGL. Штурмовал я его раза три c перерывами в год, и на третий раз наконец достиг чего-то более сложного, чем рисование разноцветных треугольников — объемные движущиеся объекты, движущаяся камера, освещение, текстуры и тени — уже, на мой взгляд, неплохо (рисунок 1).

Рисунок 1 — Солнце, Земля и Луна и Млечный Путь на заднем плане, отрисованные при помощи OpenGL. Луна вращается вокруг Земли, а Земля и Солнце — вокруг своих осей. Солнце является единственным точечным источником света. Используется модель освещения Phong lighting model плюс shadow mapping. Всё это отрисовывается программой, которую я написал сам, используя только OpenGL, стандартную библиотеку языка C++ (STL) и WinAPI.

Литература

Книги по компьютерной графике вообще и по OpenGL в частности (указаны в порядке субъективного убывания полезности):


  • Jason McKesson — Learning Modern 3D Graphics Programming
  • David Wolf — OpenGL 4 Shading Language Cookbook (2nd Edition)
  • Jason Gregory — Game Engine Architecture (2nd Edition)
  • Edward Angel, Dave Shreiner — Interactive Computer Graphics (6th Edition)
  • Dave Shreiner, Graham Sellers, John Kessenich, Bill Licea-Kane — OpenGL Programming Guide (8th Edition)
  • John Kessenich, Graham Sellers, Dave Shreiner — OpenGL Programming Guide (9th Edition)
  • Graham Sellers — OpenGL SuperBible (7th Edition)
  • OpenGL 4.5 API and Shading Language Reference Pages

Книги по C++ (все перечисленные, на мой взгляд, одинаково полезны):

  • Бьярн Страуструп — Программирование. Принципы и практика с использованием C++
  • Bjarne Stroustrup. The C++ Programming Language. Fourth Edition.
  • Скотт Мейерс — Эффективное использование C++
  • Скотт Мейерс — Наиболее эффективное использование C++
  • Scott Meyers — Effective Modern C++

OpenGL — это C API, т. е. пользоваться им проще всего в программах на языках C/C++. Авторы книг ставят перед собой цель продемонстрировать непосредственное использование этого API, поэтому в книгах по OpenGL весь код как правило написан на языке Си, причем он длинный, трудночитаемый и трудноизменяемый. Я хотел сравнительно легко и быстро писать графические программы, поэтому должен был написать свой API поверх OpenGL, и конечно, на C++ (далее в тексте я, говоря о своем проекте, могу называть его по-разному: API, движок, фреймворк). Когда создаешь свой API, хорошо бывает применить top-down approach, т. е. проектирование «сверху вниз». Я этот подход описываю так: представьте, как должна выглядеть ваша программа, когда ваш API уже полностью написан. Запишите эту программу — в ней будут конечно же создаваться объекты, вызываться методы классов… Вот, теперь вы знаете какие классы вам нужны и какие у них должны быть методы — напишите же их!

В написании собственного API мне очень помогла великолепная книжка [Gregory]. Что касается книг по API OpenGL… Официальное руководство по программированию (т. н. красная книга) [OpenGL Programming Guide], как мне кажется, не очень годится для изучения, оно скорее напоминает справочник, и читать его ужасно скучно. К счастью я нашел минимум две книги «с человеческим лицом». [McKesson] хорошо объясняет основы компьютерной графики (графический конвейер, камера, перспективная проекция, матрицы, шейдеры, освещение, текстуры) — эту книгу приятно и интересно читать (правда в ней используется OpenGL 3.3, и некоторые функции, используемые в книге, можно уже заменить на более новые из OpenGL 4). Книжка [Wolf] — это собрание практических примеров реализации той или иной техники создания визуальных эффектов (освещение, текстурирование, тени, blending и пр.), написанное очень понятным языком.

Надо заметить, что практически во всех книгах авторы пользуются вспомогательными библиотеками для работы с оконным интерфейсом операционной системы — чаще всего это библиотеки FreeGLUT или GLFW. Для векторных вычислений используют библиотеку GLM. Я же принципиально не использовал никакие библиотеки (библиотека GLEW не в счет — она лишь загружает функции OpenGL), поскольку хотел освоить буквально все аспекты программирования графических приложений с нуля. Поэтому в дальнейших заметках речь пойдет, помимо прочего, и о создании окон при помощи WinAPI, и о векторных вычислениях.

Создание проекта

Я создал проект в MS Visual Studio 2015, назвал его RenderingEngine. Проект размещен в открытом репозитории на сайте BitBucket (о том как клонировать репозиторий и прочее см. заметку про Git). В этой и последующих заметках я буду приводить фрагменты исходного кода, которые существенны для обсуждения. Привести весь исходный код, прокомментировав каждую строчку невозможно, да и смысла нет. Если вам понадобятся подробности кода, которых нет в заметках, загляните в репозиторий.

Прежде всего я хотел «организовать рабочее пространство» — разложить всё по папочкам. Почитал некоторые рекомендации в интернете (StackOverflow — VC2010 C++ — organizing source files) и решил создать четыре папки:

Позднее я добавил еще две папки:

include для хранения заголовочных файлов сторонних библиотек
lib для хранения двоичных файлов сторонних библиотек
src для хранения моего собственного кода (как заголовочных файлов, так и файлов исходного кода)
build для хранения двоичного файла моей программы

Чтобы изменить выходную папку, в которую помещается построенный исполняемый файл программы (по-умолчанию он помещается в папку $(SolutionDir)\$(Configuration)), я должен был поменять некоторые свойства проекта (через меню Project->Properties в Visual Studio):

Project -> Properties -> General -> Output Directory
$(SolutionDir)\build\$(Configuration)\

Project -> Properties -> Debugging -> Working Directory
$(OutDir)

$(SolutionDir), $(Configuration), $(OutDir) и прочее — это так называемые макросы для команд и свойств построения. При построении проекта они подменяются своими значениям. Например, $(Configuration) заменяется на Debug или Release.

Также надо было добавить папку include в пути поиска заголовочных файлов

Пространства имен

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

shaders для хранения шейдеров
textures для хранения текстур
opengl Несмотря на то, что API OpenGL основан на языке C, он, тем не менее — объектно-ориентированный в том же смысле, в каком объектно-ориентированным является и WinAPI. Сплошь и рядом функции OpenGL создают какие-то объекты и возвращают их так называемые дескрипторы (или хэндлы) — числовые идентификаторы, при помощи которых программа может на эти объекты ссылаться. В общем, API OpenGL легко и естественно можно перевести в объектно-ориентированный сиплюсплюсный вид, что я и сделал, создав такие классы как Shader, Buffer, FramebufferObject, Renderbuffer, ProgramPipeline и другие и поместив их в пространство имен opengl.
win В это пространство имен я поместил все классы и функции, которые используют WinAPI и поэтому являются специфичными для ОС Windows. Например, классы Window, HighResolutionTimer, winapi_error и другие.
engine В это пространство имен я помещал классы, которые воспринимаются мной как «высокий уровень абстракции». Например классы Camera, Mesh, Model3D, Skybox, ShadowMap и другие. Эти классы зависят от классов в пространстве имен opengl.
util В это пространство имен я помещал классы и функции, которые выполняли роль полезных в хозяйстве инструментов. Типичный пример — функции из пространства имен util::Requires, которые например проверяют корректность аргументов, переданных в функцию и другие условия. Также в пространство имен util попали функции file::ReadAllText, file::getFileExtension, класс Event и пр.
vmath Сюда попали классы Vector и Matrix и всевозможные функции для работы с матрицами и векторами. Эти классы зависят от платформы Intel, поскольку используют Streaming SIMD Extensions (SSE), впрочем использование SSE можно отключить при помощи условной компиляции.

Замечу, что среди приведенных пространств имен только пространство win содержит платформозависимый код. Во всяком случае, я надеюсь, что это так. Приведенные выше пространства имен отражают модульность программы. Эти пространства не зависят друг от друга (за исключением engine, которое зависит от всех остальных пространств имен) и поэтому их можно разрабатывать независимо, а это — большой плюс для проекта.

Загрузка функций OpenGL

Итак, я хочу использовать в своей программе некий API на языке C. Обычно это значит вот что. API представляет собой набор функций. Прототипы (сигнатуры) этих функций находятся в некоем заголовочном файле (или файлах), а сами функции находятся в двоичном файле библиотеки (в ОС Windows такие файлы имеют расширение .lib или .dll). Также существует документация, в которой написано, что каждая функция делает и как вообще эти функции следует использовать.

В OpenGL это не совсем так. В каждой ОС существует библиотека, которая содержит ограниченный набор OpenGL’евских функций. В Windows это библиотека opengl32.dll, которая содержит набор функций, соответствующих устаревшей сто лет назад версии OpenGL 1.1. С тех пор огромное количество новых функций было добавлено в API, к тому же многие функции теперь объявлены устаревшими (т. е. ими буквально нельзя пользоваться). Предполагается загружать новые функции динамически, т. е. получать адреса функций во время выполнения программы. Для этого в OpenGL всегда был предусмотрен так называемый OpenGL extension mechanism — механизм, позволяющий производителям видеокарт добавлять в API новые функции, а пользователям (программистам) — эти функции вызывать. Но если эти функции не находятся в opengl32.dll, то где они тогда лежат? Ответ: в библиотеке, которая входит в состав ПО, которое поставляется вместе с вашей видеокартой. Например для видеокарт NVIDIA файл OpenGL’евской библиотеки может называться как-то так: nvogl32.dll. Однако программист, разумеется, не должен компоновать свою программу с библиотекой производителя видеокарты (в противном случае его программа была бы плохо переносимой), вместо этого программа запрашивает указатели на функции API во время выполнения. Каким образом это делается, подробно описано в статье Load OpenGL Functions. Скажу только, что в Windows для получения указателя на какую либо функцию OpenGL используется функция под названием wglGetProcAddress (заголовочный файл wingdi.h, библиотека opengl32.dll; все функции, начинающиеся с wgl, относятся к ОС Windows). Функция wglGetProcAddress принимает в качестве параметра имя искомой функции. Что происходит у wglGetProcAddress «под капотом» для меня — тайна, покрытая мраком. Загружает ли она в память приложения библиотеку от производителя видеокарты и если да, то как она ее находит? Почему для ее вызова необходимо создать контекст OpenGL (об этом ниже)? Впрочем это не столь важно, так как на самом деле рекомендуется перепоручать загрузку всех функций OpenGL специальным библиотекам, которые называются OpenGL Loading Libraries. Одной из таких библиотек является OpenGL Extension Wrangler Library (GLEW), которую я и использовал в своем проекте.

Библиотека OpenGL Extension Wrangler Library (GLEW)

Библиотека GLEW предназначена для загрузки функций API OpenGL. Что это значит? Предположим, вам надо вызвать какую-нибудь OpenGL’евскую функцию, например glAttachShader. Чтобы вызвать функцию, вам нужен ее адрес. Адрес функции можно получить при помощи wglGetProcAddress. Поскольку вы собираетесь вызывать функцию glAttachShader много раз в разных местах программы, ее адрес надо сохранить в некой глобальной переменной. Эта переменная должна иметь тип указателя на функцию, сигнатура которой совпадает с сигнатурой функции glAttachShader, описанной в спецификации OpenGL. И таких функций как glAttachShader — сотни. Для каждой из них в библиотеке GLEW имеется глобальная переменная, которая хранит адрес функции. А значения этим переменным присваиваются функцией glewInit. Ниже показан фрагмент заголовочного файла glew.h, в котором объявлены прототип функции glAttachShader и указатель на нее. Я снабдил этот фрагмент своими комментариями для большей понимабельности.

// The most important function in GLEW library which fills a bunch of global function pointers
// with the adresses of OpenGL functions.
GLEWAPI GLenum GLEWAPIENTRY glewInit ( void ) ;

// No idea what this is used for 🙂
#ifndef GLEW_GET_FUN
#define GLEW_GET_FUN(x) x
#endif

// Pointer-to-function type definition.
typedef void ( GLAPIENTRY * PFNGLATTACHSHADERPROC ) ( GLuint program, GLuint shader ) ;

// The pointer to function glAttachShader.
GLEW_FUN_EXPORT PFNGLATTACHSHADERPROC __glewAttachShader ;

// Macrodefinition which translates all references to glAttachShader to __glewAttachShader.
#define glAttachShader GLEW_GET_FUN(__glewAttachShader)

Итак, чтобы воспользоваться библиотекой GLEW, надо

    Скачать библиотеку GLEW с оф. сайта. Из скачанного архива мне нужны только четыре файла: glew.h, wglew.h (этот заголовочный файл нужен, если программа предназначается для ОС Windows), glew32.lib и glew32.dll, которые я распихиваю в папки своего проекта. glew.h, wglew.h — в папку \include\GL , glew32.lib — в папку \lib\GLEW , glew32.dll — в папки \build\Debug и \build\Release . В своем проекте я использую динамическую компоновку с библиотекой glew32.dll, однако можно использовать и статическую — тогда файл glew32.dll не нужен, а вместо файла glew32.lib надо взять файл glew32s.lib. Включение заголовочных файлов GLEW и подключение библиотек glew32.lib и opengl32.lib я поместил в файл GLEWHeaders.h:

// GLEWHeaders.h
#pragma once

// Link to opengl32.lib
#pragma comment(lib, «opengl32.lib»)

// Link to glew32.lib
#pragma comment(lib, «glew32.lib»)

// You can also statically link GLEW library as follows:
// #pragma comment(lib, «glew32s.lib»)
// #define GLEW_STATIC

#include «GL/glew.h»
#include «GL/wglew.h»

С вызовом glewInit связаны некоторые трудности. Оказывается, для того, чтобы вызвать glewInit, надо сначала создать так называемый OpenGL rendering context (далее — rendering context или контекст рисования). Функция wglGetProcAddress тоже не будет работать без созданного rendering context’а. Что же это такое — контекст рисования? Попробую дать определение.

  • Rendering context представляет собой некий объект, обладающий состоянием. Это состояние влияет на результат (или управляет результатом) работы функций OpenGL. В частности, состояние, в котором находится rendering context, определяет изображение, которое формируется на экране компьютера. Вызовы функций OpenGL могут изменять состояние rendering context’а. Пользовательская программа может взаимодействовать с rendering context’ом только при помощи функций OpenGL.
  • Могу предположить, что rendering context хранится в памяти драйвера видеокарты.
  • Пользовательская программа может создавать (функции wglCreateContext и wglCreateContextAttribsARB) и уничтожать (функция wglDeleteContext) rendering context. При создании контекста программа указывает, каким требованиям он должен удовлетворять (например, поддерживать определенную версию OpenGL).
  • Rendering context как правило существует в приложении в единственном экземпляре. Однако можно создать в одной программе несколько rendering context’ов. Если в программе есть несколько rendering context’ов, то в конкретный момент времени только один из них может быть активным (или текущим). Активный rendering context — это тот, который в данный момент влияет на результат работы функций OpenGL (т. е. например на отрисовку изображения на экране) и на состояние которого влияют вызовы функций OpenGL. Программа может переключаться между несколькими rendering context’ами (делать текущим то один, то другой контекст — см. функцию wglMakeCurrent). Понятие «активный контекст» принадлежит потоку управления, т. е. каждый поток управления может иметь свой активный контекст OpenGL. Заметим, что нежелательно, чтобы два разных потока управления имели одинаковый активный контекст OpenGL.
  • В Windows rendering context связан с так называемым контекстом устройства (device context — даже не хочу разбираться, что это такое, можете почитать статью Device Contexts) или проще говоря — с окном. Без окна нельзя создать контекст рисования. Соответственно, в этом окне и будет отрисовываться изображение, формируемое программой путем вызова различных функции OpenGL. При этом во время выполнения программы можно связывать любой контекст рисования с любым контекстом устройства. Можно даже связать один контекст рисования с несколькими контекстами устройств (но не наоборот) — см. функцию wglMakeCurrent. Тогда вы получите одно и то же изображение, отрисованное в нескольких окнах.
  • Без контекста рисования нельзя ничего нарисовать на экране компьютера.

Почему надо непременно создать контекст рисования, чтобы вызвать glewInit? Возможно это связано с тем, что в Windows значения указателей на функции, которые возвращает wglGetProcAddress, зависят от текущего контекста рисования… О’кей, давайте создадим этот контекст, тем более, что для рисования все равно нужен контекст рисования. Но оказывается, что контекст контексту рознь. По-настоящему работающий контекст рисования (при помощи которого можно что-то нарисовать) можно создать только при помощи функций wglChoosePixelFormatARB и wglCreateContextAttribsARB. Но адресов этих функций у нас нет, а чтобы их получить, нам надо либо вызвать glewInit либо вызвать wglGetProcAddress, но ни то, ни другое нельзя сделать без контекста рисования. Замкнутый круг? К счастью существует старинная функция для создания контекста рисования wglCreateContext, которая экспортируется непосредственно библиотекой opengl32.dll и поэтому ее адрес у нас есть изначально. Хотя такой контекст рисования будет поддерживать только версию OpenGL 1.1, и рисовать что-либо с его помощью нецелесообразно, он позволит нам вызвать glewInit. Затем мы его удалим и создадим новый нормальный контекст. Всю работу по инициализации GLEW я поместил в функцию Initialize_GLEW_Library в файлах GlewInitializer.h и GlewInitializer.cpp (см. листинг ниже).
Итак, алгоритм инициализации библиотеки GLEW следующий:

  • Создать окно. Окно нужно, так как без него нельзя создать контекст рисования. Кстати, созданное окно мы не отображаем на экране.
  • Создать контекст рисования при помощи функции wglCreateContext. Сделать его активным при помощи функции wglMakeCurrent.
  • Вызвать функцию glewInit.
  • Уничтожить окно. Для этого надо послать ему сообщение WM_DESTROY при помощи функции DestroyWindow. Это сообщение надо еще и обработать в цикле обработки сообщений, именно поэтому, несмотря на то, что окно мы нигде не отображаем, цикл обработки сообщений запускать все же приходится.
  • Уничтожить созданный ранее контекст рисования (поскольку он больше не нужен) при помощи функции wglDeleteContext.

// GlewInitializer.h
#pragma once

namespace opengl
<
// Initializes GLEW library
void Initialize_GLEW_Library ( ) ;
>

#include
#include «GlewInitializer.h»
#include «winapi_error.h»
#include «GlException.h»

namespace opengl
<
LRESULT CALLBACK FalseWndProc (
HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam )
<
switch ( message )
<
case WM_DESTROY :
<
PostQuitMessage ( 0 ) ;
break ;
>
default :
<
return DefWindowProc ( hWnd, message, wParam, lParam ) ;
>
>

HGLRC CreateFalseRenderingContext ( HDC hDC )
<
PIXELFORMATDESCRIPTOR pfd ;

// Choose a stub pixel format in order to get access to wgl functions.
:: SetPixelFormat (
hDC, // Device context.
1 , // Index that identifies the pixel format to set. The various pixel formats supported by a device context are identified by one-based indexes.
& pfd ) ; // [out] Pointer to a PIXELFORMATDESCRIPTOR structure that contains the logical pixel format specification.

// Create a fiction OpenGL rendering context.
HGLRC hGLRC = wglCreateContext ( hDC ) ;

// Throw exception if failed to create OpenGL rendering context.
if ( hGLRC == NULL )
throw win :: make_winapi_error ( «opengl::CreateFalseRenderingContext() -> wglCreateContext()» ) ;

// Make just created OpenGL rendering context current.
if ( ! wglMakeCurrent ( hDC, hGLRC ) )
throw win :: make_winapi_error ( «opengl::CreateFalseRenderingContext() -> wglMakeCurrent()» ) ;

void Initialize_GLEW_Library ( )
<
static bool s_IsInitialized = false ;

if ( s_IsInitialized )
return ;

const char * wnd >= «FalseWindow» ;
HINSTANCE hInst = GetModuleHandle ( NULL ) ; // get application handle

// Create a struct describing window class.
WND >;
wcex. cbSize = sizeof ( WND >) ; // struct size
wcex. style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC ; // window style
wcex. lpfnWndProc = FalseWndProc ; // pointer to window function WndProc
wcex. cbClsExtra = 0 ; // shared memory
wcex. cbWndExtra = 0 ; // number of additional bytes
wcex. hInstance = hInst ; // current application’s handle
wcex. hIcon = LoadIcon ( hInst, MAKEINTRESOURCE ( >) ) ; // icon handle
wcex. hCursor = LoadCursor ( NULL , >) ; // cursor handle
wcex. hbrBackground = ( HBRUSH ) ( COLOR_MENU + 1 ) ; // background brush’s handle
wcex. lpszMenuName = NULL ; // pointer to a string — menu name
wcex. lpszClassName = wnd >; // pointer to a string — window class name
wcex. hIconSm = LoadIcon ( hInst, MAKEINTRESOURCE ( >) ) ; // small icon’s handle

// Create a FICTION window based on the previously registered window class.
HWND hWnd = CreateWindow ( wnd >// window class name
wnd >// window title
WS_OVERLAPPEDWINDOW, // window type
CW_USEDEFAULT, CW_USEDEFAULT, // window’s start position (x, y)
100 , // window’s width in pixels
100 , // window’s height in pixels
NULL , // parent window
NULL , // menu handle
hInst, // application handle
NULL ) ; // pointer to an object passed to the window with CREATESTRUCT struct (field lpCreateParams), pointer to which is contained in lParam parameter of WM_CREATE message

// Throw exception if CreateWindow() failed.
if ( ! hWnd )
throw win :: make_winapi_error ( «opengl::Initialize_GLEW_Library() -> CreateWindow()» ) ;

// Get device context for the window.
HDC hDC = GetDC ( hWnd ) ;

// Create a fiction rendering context.
HGLRC tempOpenGLContext = CreateFalseRenderingContext ( hDC ) ;

// Initialize GLEW (is possible only if an OpenGL rendering context is created).
if ( glewInit ( ) ! = GLEW_OK )
throw GlException ( «opengl::Initialize_GLEW_Library() -> glewInit()» ) ;

wglMakeCurrent ( NULL , NULL ) ; // remove the temporary context from being active
wglDeleteContext ( tempOpenGLContext ) ; // delete the temporary OpenGL context
>
>

В приведенном коде используются классы win::winapi_error и opengl::GlException — это всего-навсего классы исключений для ошибок, связанных соответственно с WinAPI и OpenGL, приводить их код здесь не буду.

На этом все. В следующей заметке я расскажу о том, как создать окно (рисовать-то мы будем именно в окне).

Графическая библиотека OpenGL

методическое пособие


Авторы: Юрий Баяковский
Игнатенко Алексей
Фролов Антон
Источник: Лаборатория компьютерной графики при ВМиК МГУ

Опубликовано: 17.09.2002
Исправлено: 13.03.2005
Версия текста: 2.0

Введение

OpenGL является одним из самых популярных прикладных программных интерфейсов (API – Application Programming Interface) для разработки приложений в области двумерной и трехмерной графики.

Стандарт OpenGL (Open Graphics Library – открытая графическая библиотека) был разработан и утвержден в 1992 году ведущими фирмами в области разработки программного обеспечения как эффективный аппаратно-независимый интерфейс, пригодный для реализации на различных платформах. Основой стандарта стала библиотека IRIS GL, разработанная фирмой Silicon Graphics Inc.

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

На сегодняшний день графическая система OpenGL поддерживается большинством производителей аппаратных и программных платформ. Эта система доступна тем, кто работает в среде Windows, пользователям компьютеров Apple. Свободно распространяемые коды системы Mesa (пакет API на базе OpenGL) можно компилировать в большинстве операционных систем, в том числе в Linux.

Характерными особенностями OpenGL, которые обеспечили распространение и развитие этого графического стандарта, являются:

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

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

Основы OpenGL


Основные возможности

Описывать возможности OpenGL мы будем через функции его библиотеки. Все функции можно разделить на пять категорий:

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

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

Интерфейс OpenGL

OpenGL состоит из набора библиотек. Все базовые функции хранятся в основной библиотеке, для обозначения которой в дальнейшем мы будем использовать аббревиатуру GL. Помимо основной, OpenGL включает в себя несколько дополнительных библиотек.

Первая из них – библиотека утилит GL ( GLU – GL Utility ). Все функции этой библиотеки определены через базовые функции GL. В состав GLU вошла реализация более сложных функций, таких как набор популярных геометрических примитивов (куб, шар, цилиндр, диск), функции построения сплайнов, реализация дополнительных операций над матрицами и т.п.

OpenGL не включает в себя никаких специальных команд для работы с окнами или ввода информации от пользователя. Поэтому были созданы специальные переносимые библиотеки для обеспечения часто используемых функций взаимодействия с пользователем и для отображения информации с помощью оконной подсистемы. Наиболее популярной является библиотека GLUT (GL Utility Toolkit). Формально GLUT не входит в OpenGL, но de facto включается почти во все его дистрибутивы и имеет реализации для различных платформ. GLUT предоставляет только минимально необходимый набор функций для создания OpenGL-приложения. Функционально аналогичная библиотека GLX менее популярна. В дальнейшем в этом пособии в качестве основной будет рассматриваться GLUT.

Рис. 1 Организация библиотеки OpenGL

Кроме того, функции, специфичные для конкретной оконной подсистемы, обычно входят в ее прикладной программный интерфейс. Так, функции, поддерживающие выполнение OpenGL, есть в составе Win32 API и X Window. На рисунке схематически представлена организация системы библиотек в версии, работающей под управлением системы Windows. Аналогичная организация используется и в других версиях OpenGL.

Архитектура OpenGL

Функции OpenGL реализованы в модели клиент-сервер. Приложение выступает в роли клиента – оно вырабатывает команды, а сервер OpenGL интерпретирует и выполняет их. Сам сервер может находиться как на том же компьютере, на котором находится клиент (например, в виде динамически загружаемой библиотеки – DLL), так и на другом (при этом может быть использован специальный протокол передачи данных между машинами).

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

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

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

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

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

Рис. 2. Функционирование конвейера OpenGL

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

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

Синтаксис команд

Определения команд GL находятся в файле gl.h, для включения которого нужно написать

Для работы с библиотекой GLU нужно аналогично включить файл glu.h. Версии этих библиотек, как правило, включаются в дистрибутивы систем программирования, например Microsoft Visual C++ или Borland C++ 5.02.

В отличие от стандартных библиотек, пакет GLUT нужно инсталлировать и подключать отдельно. Подробная информация о настройке сред программирования для работы с OpenGL дана в Приложении С.

Все команды (процедуры и функции) библиотеки GL начинаются с префикса gl, все константы – с префикса GL_. Соответствующие команды и константы библиотек GLU и GLUT аналогично имеют префиксы glu (GLU_) и glut (GLUT_)

Кроме того, в имена команд входят суффиксы, несущие информацию о числе и типе передаваемых параметров. В OpenGL полное имя команды имеет вид:

Таким образом, имя состоит из нескольких частей:

Gl это имя библиотеки, в которой описана эта функция: для базовых функций OpenGL, функций из библиотек GLU, GLUT, GLAUX это gl, glu, glut, glaux соответственно
Command_name имя команды
[1 2 3 4] число аргументов команды
[b s i f d ub us ui] тип аргумента: символ b означает тип GLbyte (аналог char в С\С++), символ f – тип GLfloat (аналог float), символ i – тип GLint (аналог int) и так далее. Полный список типов и их описание можно посмотреть в файле gl.h
[v] наличие этого символа показывает, что в качестве параметров функции используется указатель на массив значений

Символы в квадратных скобках в некоторых названиях не используются. Например, команда glVertex2i() описана как базовая в библиотеке OpenGL, и использует в качестве параметров два целых числа, а команда glColor3fv() использует в качестве параметра указатель на массив из трех вещественных чисел.

Использования нескольких вариантов каждой команды можно частично избежать, применяя перегрузку функций языка C++. Но интерфейс OpenGL не рассчитан на конкретный язык программирования, и, следовательно, должен быть максимально универсален.

Пример приложения

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

Ниже приведен текст небольшой программы, написанной с использованием библиотеки GLUT – своеобразный аналог классического примера “Hello, World!”.

Все, что делает эта программа – рисует в центре окна красный квадрат. Тем не менее, даже на этом простом примере можно понять принципы программирования с помощью OpenGL.

Несмотря на малый размер, это полностью завершенная программа, которая должна компилироваться и работать на любой системе, поддерживающей OpenGL и GLUT.

Библиотека GLUT поддерживает взаимодействие с пользователем с помощью так называемых функций c обратным вызовом ( callback function ). Если пользователь подвинул мышь, нажал на кнопку клавиатуры или изменил размеры окна, происходит событие и вызывается соответствующая функция пользователя – обработчик событий (функция с обратным вызовом).

Рассмотрим более подробно функцию main данного примера. Она состоит из трех частей – инициализации окна, в котором будет рисовать OpenGL, настройки функций c обратным вызовом и главного цикла обработки событий.


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

Функция glutInit(&argc, argv) производит начальную инициализацию самой библиотеки GLUT.

Команда glutInitDisplayMode(GLUT_RGB) инициализирует буфер кадра и настраивает полноцветный (непалитровый) режим RGB.

glutInitWindowSize(Width, Height) используется для задания начальных размеров окна.

Наконец, glutCreateWindow(«Red square example») задает заголовок окна и визуализирует само окно на экране.

регистрируют функции Display(), Reshape() и Keyboard() как функции, которые будут вызваны, соответственно, при перерисовке окна, изменении размеров окна, нажатии клавиши на клавиатуре.

Контроль всех событий и вызов нужных функций происходит внутри бесконечного цикла в функции glutMainLoop()

Заметим, что библиотека GLUT не входит в состав OpenGL, а является лишь переносимой прослойкой между OpenGL и оконной подсистемой, предоставляя минимальный интерфейс. OpenGL-приложение для конкретной платформы может быть написано с использованием специфических API (Win32, X Window и т.д.), которые как правило предоставляют более широкие возможности.

Более подробно работа с библиотекой GLUT описана в Приложении А.

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

Следующая последовательность команд из функции Display

очищает окно и выводит на экран квадрат, задавая координаты четырех угловых вершин и цвет.

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

Контрольные вопросы:

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

Кратко опишите архитектуру библиотек OpenGL и организацию конвейера.

В чем заключаются функции библиотек, подобных GLUT или GLX? Почему они формально не входят в OpenGL?

Назовите категории команд (функций) библиотеки.

Почему организацию OpenGL часто сравнивают с конечным автоматом?

Зачем нужны различные варианты команд OpenGL, отличающиеся только типами параметров?

Что можно сказать о количестве и типе параметров команды glColor4ub()? glVertex3fv()?

Рисование геометрических объектов


Процесс обновления изображения

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

Обычно приложение OpenGL в бесконечном цикле вызывает функцию обновления изображения в окне. В этой функции и сосредоточены вызовы основных команд OpenGL. Если используется библиотека GLUT, то это будет функция с обратным вызовом, зарегистрированная с помощью вызова glutDisplayFunc(). GLUT вызывает эту функцию, когда операционная система информирует приложение о том, что содержимое окна необходимо перерисовать (например, если окно было перекрыто другим). Создаваемое изображение может быть как статичным, так и анимированным, т.е. зависеть от каких-либо параметров, изменяющихся со временем. В этом случае лучше вызывать функцию обновления самостоятельно. Например, с помощью команды glutPostRedisplay(). За более подробной информацией можно обратиться к приложению A.

Приступим, наконец, к тому, чем занимается типичная функция обновления изображения. Как правило, она состоит из трех шагов:

очистка буферов OpenGL;

установка положения наблюдателя;

преобразование и рисование геометрических объектов.

Очистка буферов производится с помощью команды:

Команда glClearColor устанавливает цвет, которым будет заполнен буфер кадра. Первые три параметра команды задают R,G и B компоненты цвета и должны принадлежать отрезку [0,1]. Четвертый параметр задает так называемую альфа компоненту (см. п. 0). Как правило, он равен 1. По умолчанию цвет – черный (0,0,0,1).

Команда glClear очищает буферы, а параметр buf определяет комбинацию констант, соответствующую буферам, которые нужно очистить (см. главу 6). Типичная программа вызывает команду

для очистки буферов цвета и глубины.

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

Сейчас сосредоточимся на том, как передать в OpenGL описания объектов, находящихся в сцене. Каждый объект является набором примитивов OpenGL.

Вершины и примитивы

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

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

Положение вершины в пространстве

Положение вершины определяются заданием ее координат в двух-, трех-, или четырехмерном пространстве (однородные координаты). Это реализуется с помощью нескольких вариантов команды glVertex*:

Каждая команда задает четыре координаты вершины: x, y, z, w. Команда glVertex2* получает значения x и y. Координата z в таком случае устанавливается по умолчанию равной 0, координата w – равной 1. Vertex3* получает координаты x, y, z и заносит в координату w значение 1. Vertex4* позволяет задать все четыре координаты.

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

Цвет вершины

Для задания текущего цвета вершины используются команды :

Первые три параметра задают R, G, B компоненты цвета, а последний параметр определяет коэффициент непрозрачности (так называемая альфа-компонента). Если в названии команды указан тип ‘f’ (float), то значения всех параметров должны принадлежать отрезку [0,1], при этом по умолчанию значение альфа-компоненты устанавливается равным 1.0, что соответствует полной непрозрачности. Тип ‘ub’ (unsigned byte) подразумевает, что значения должны лежать в отрезке [0,255].

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

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

вызов которой с параметром GL_SMOOTH включает интерполяцию (установка по умолчанию), а с GL_FLAT – отключает.

Нормаль

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

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

Режим автоматической нормализации должен быть включен, если приложение использует модельные преобразования растяжения/сжатия, так как в этом случае длина нормалей изменяется при умножении на модельно-видовую матрицу.

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

Отметим, что команды

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

Операторные скобки glBegin / glEnd

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

Параметр mode определяет тип примитива, который задается внутри и может принимать следующие значения:

GL_POINTS каждая вершина задает координаты некоторой точки.
GL_LINES каждая отдельная пара вершин определяет отрезок; если задано нечетное число вершин, то последняя вершина игнорируется.
GL_LINE_STRIP каждая следующая вершина задает отрезок вместе с предыдущей.
GL_LINE_LOOP отличие от предыдущего примитива только в том, что последний отрезок определяется последней и первой вершиной, образуя замкнутую ломаную.
GL_TRIANGLES каждая отдельная тройка вершин определяет треугольник; если задано не кратное трем число вершин, то последние вершины игнорируются.
GL_TRIANGLE_STRIP каждая следующая вершина задает треугольник вместе с двумя предыдущими.
GL_TRIANGLE_FAN треугольники задаются первой и каждой следующей парой вершин (пары не пересекаются).
GL_QUADS каждая отдельная четверка вершин определяет четырехугольник; если задано не кратное четырем число вершин, то последние вершины игнорируются.
GL_QUAD_STRIP четырехугольник с номером n определяется вершинами с номерами 2n-1, 2n, 2n+2, 2n+1.
GL_POLYGON последовательно задаются вершины выпуклого многоугольника.

Например, чтобы нарисовать треугольник с разными цветами в вершинах, достаточно написать:

Как правило, разные типы примитивов имеют различную скорость визуализации на разных платформах. Для увеличения производительности предпочтительнее использовать примитивы, требующие меньшее количество информации для передачи на сервер, такие как GL_TRIANGLE_STRIP , GL_QUAD_STRIP , GL_TRIAGLE_FAN.

Рис. 3. Примитивы OpenGL

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

Однако сначала надо определить понятие лицевых и обратных граней.

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

со значением параметра mode равным GL_CW (clockwise), а вернуть значение по умолчанию можно, указав GL_CCW (counter-clockwise).

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

Параметр mode определяет, как будут отображаться многоугольники, а параметр face устанавливает тип многоугольников, к которым будет применяться эта команда и может принимать следующие значения:

GL_FRONT для лицевых граней
GL_BACK для обратных граней
GL_FRONT_AND_BACK для всех граней

Параметр mode может быть равен:

GL_POINT при таком режиме будут отображаться только вершины многоугольников.
GL_LINE при таком режиме многоугольник будет представляться набором отрезков.
GL_FILL при таком режиме многоугольники будут закрашиваться текущим цветом с учетом освещения, и этот режим установлен по умолчанию.

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

Вызов с параметром GL_FRONT приводит к удалению из изображения всех лицевых граней, а с параметром GL_BACK – обратных (установка по умолчанию).

Кроме рассмотренных стандартных примитивов в библиотеках GLU и GLUT описаны более сложные фигуры, такие как сфера, цилиндр, диск (в GLU) и сфера, куб, конус, тор, тетраэдр, додекаэдр, икосаэдр, октаэдр и чайник (в GLUT). Автоматическое наложение текстуры предусмотрено только для фигур из библиотеки GLU (создание текстур в OpenGL будет рассматриваться в главе 5).

Например, чтобы нарисовать сферу или цилиндр, надо сначала создать объект специального типа GLUquadricObj с помощью команды

а затем вызвать соответствующую команду:

где параметр slices задает число разбиений вокруг оси z, а stacks – вдоль оси z.

Более подробную информацию об этих и других командах построения примитивов можно найти в приложении В.

Дисплейные списки

Если мы несколько раз обращаемся к одной и той же группе команд, то их можно объединить в так называемый дисплейный список (display list), и вызывать его при необходимости. Для того, чтобы создать новый дисплейный список, надо поместить все команды, которые должны в него войти, между следующими операторными скобками:

Для различения списков используются целые положительные числа, задаваемые при создании списка значением параметра list , а параметр mode определяет режим обработки команд, входящих в список:

GL_COMPILE команды записываются в список без выполнения
GL_COMPILE_AND_EXECUTE команды сначала выполняются, а затем записываются в список

После того, как список создан, его можно вызвать командой

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

вызывающей n списков с идентификаторами из массива lists , тип элементов которого указывается в параметре type . Это могут быть типы GL_BYTE , GL_UNSIGNED_BYTE , GL_SHORT , GL_INT , GL_UNSIGNED_INT и некоторые другие. Для удаления списков используется команда

которая удаляет списки с идентификаторами ID из диапазона list list + range -1.

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

Массивы вершин

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

которая определяет способ хранения и координаты вершин. При этом size определяет число координат вершины (может быть равен 2, 3, 4), type определяет тип данных (может быть равен GL_SHORT , GL_INT , GL_FLOAT , GL_DOUBLE ). Иногда удобно хранить в одном массиве другие атрибуты вершины, тогда параметр stride задает смещение от координат одной вершины до координат следующей; если stride равен нулю, это значит, что координаты расположены последовательно. В параметре ptr указывается адрес, где находятся данные.

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

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

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

с соответствующим значением параметра array.

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

которая передает OpenGL атрибуты вершины, используя элементы массива с номером index . Это аналогично последовательному применению команд вида glColor*(…), glNormal*(…), glVertex*(…) c соответствующими параметрами. Однако вместо нее обычно вызывается команда

рисующая count примитивов, определяемых параметром mode , используя элементы из массивов с индексами от first до first + count -1. Это эквивалентно вызову последовательности команд glArrayElement() с соответствующими индексами.

В случае, если одна вершина входит в несколько примитивов, то вместо дублирования ее координат в массиве удобно использовать ее индекс.

Для этого надо вызвать команду

где indices – это массив номеров вершин, которые надо использовать для построения примитивов, type определяет тип элементов этого массива: GL_UNSIGNED_BYTE , GL_UNSIGNED_SHORT , GL_UNSIGNED_INT , а count задает их количество.

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

Контрольные вопросы

Что такое функция обратного вызова и как функции обратного вызова могут быть использованы для работы с OpenGL?

Для чего нужна функция обновления изображения и что она делает?

Что такое примитив в OpenGL?

Что такое атрибут? Перечислите известные вам атрибуты вершин в OpenGL.

Что в OpenGL является атомарным примитивом? Какие типы примитивов вы знаете?

Для чего в OpenGL используются команды glEnable/glDisable?

Что такое операторные скобки и для чего они используются в OpenGL?

Что такое дисплейные списки? Как определить список и как вызвать его отображение?

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

Поясните работу команды glDrawElements()

Преобразования объектов

В OpenGL используются как основные три системы координат: левосторонняя, правосторонняя и оконная. Первые две системы являются трехмерными и отличаются друг от друга направлением оси z: в правосторонней она направлена на наблюдателя, в левосторонней – в глубину экрана. Ось x направлена вправо относительно наблюдателя, ось y – вверх.

Левосторонняя система используется для задания значений параметрам команды gluPerspective(), glOrtho(), которые будут рассмотрены в пункте 0. Правосторонняя система координат используется во всех остальных случаях. Отображение трехмерной информации происходит в двумерную оконную систему координат.

Рис. 4 Системы координат в OpenGL

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

Работа с матрицами

Для задания различных преобразований объектов сцены в OpenGL используются операции над матрицами, при этом различают три типа матриц: модельно-видовая, матрица проекций и матрица текстуры. Все они имеют размер 4×4. Видовая матрица определяет преобразования объекта в мировых координатах, такие как параллельный перенос, изменение масштаба и поворот. Матрица проекций определяет, как будут проецироваться трехмерные объекты на плоскость экрана (в оконные координаты), а матрица текстуры определяет наложение текстуры на объект.


Умножение координат на матрицы происходит в момент вызова соответствующей команды OpenGL, определяющей координату (как правило, это команда glVertex*)

Для того чтобы выбрать, какую матрицу надо изменить, используется команда:

вызов которой со значением параметра mode равным GL_MODELVIEW , GL_PROJECTION , или GL_TEXTURE включает режим работы с модельно-видовой матрицей, матрицей проекций, или матрицей текстуры соответственно. Для вызова команд, задающих матрицы того или иного типа, необходимо сначала установить соответствующий режим.

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

где m указывает на массив из 16 элементов типа float или double в соответствии с названием команды, при этом сначала в нем должен быть записан первый столбец матрицы, затем второй, третий и четвертый. Еще раз обратим внимание: в массиве m матрица записана по столбцам .

заменяет текущую матрицу на единичную.

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

Они записывают и восстанавливают текущую матрицу из стека, причем для каждого типа матриц стек свой. Для модельно-видовых матриц его глубина равна как минимум 32, для остальных – как минимум 2.

Для умножения текущей матрицы на другую матрицу используется команда

где параметр m должен задавать матрицу размером 4×4. Если обозначить текущую матрицу за М, передаваемую матрицу за T, то в результате выполнения команды glMultMatrix текущей становится матрица M * T. Однако обычно для изменения матрицы того или иного типа удобно использовать специальные команды, которые по значениям своих параметров создают нужную матрицу и умножают ее на текущую.

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

Рис. 5. Преобразования координат в OpenGL

Свойства задаются для обеих сторон поверхности.

Параметр pname задает, какое свойство материала передается через params . Ниже приведена таблица возможных значений pname со значениями по умолчанию и краткими пояснениями. Значения по умолчанию для лицевой и тыльной сторон совпадают.

Параметр pname команды glMaterialfv

Краткий комментарий

цвет фонового и рассеянного отражения материала

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

Для установки коэффициента блеска можно использовать команду:

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

Прим. Несмотря на то, что α-компонента имеется у всех свойств отражения, учитывается только значение для рассеянной компоненты.


Практический взгляд на цвет

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

Попробуем разобраться, что такое «цвет материала» и «цвет источника света». В реальном мире любой источник света имеет спектр, который характеризует, свет в каком диапазоне длин волн излучает этот источник. Точно так же материал по-разному отражает/преломляет/поглощает свет с различной длиной волны. Если освещать зеленый предмет красным светом, то он будет казаться серым. В модели освещения OpenGL все идентично, за той лишь разницей, что можно задавать степень отражения для красной, зеленой и синей компонент составляющих освещенности.

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

Если пронаблюдать, как блики ложатся на реальные объекты, то вы увидите, что цвет блика совпадает с цветом источника света и практически не зависит от цвета поверхности. Поэтому зеркальное отражение материала имеет смысл задавать серым цветом, и варьировать цвет бликов, задавая зеркальную компоненту для источника света. Остроту бликов можно варьировать, изменяя коэффициент блеска.

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

Управление свойствами материала с помощью команды glColor4f

Может возникнуть такая ситуация, что требуется часто изменять одно из свойств материала, к примеру, рассеянное отражение. На этот случай в OpenGL предусмотрен удобный механизм. По умолчанию, при включении расчета освещения, текущий цвет, задаваемый командой glColor4f, игнорируется. Но, включив управление свойством материала с помощью текущего цвета (color-driven materials), можно изменять одну из характеристик отражения материала командой glColor4f. Это удобнее и может на некоторых реализациях работать быстрее при частом изменении свойств материала. Включается управление командой:

Задать, какое конкретно свойство материала будет передаваться текущим цветом, можно командой:

Параметр face команды glColorMaterial

Свойства задаются для лицевой стороны поверхности .

Свойства задаются для обеих сторон поверхности. Значение по умолчанию.

Параметр mode команды glColorMaterial

Управление собственным излучением материала .

Одновременное управление фоновым и рассеянным свойствами отражения. Значение по умолчанию.

H OpenGL-Tutorial. Урок 1. Создание окна в черновиках Перевод Tutorial

.collapse»>Содержание

Предисловие

Содержание

Базовые уроки:

  • Урок 1. Создание окна
  • Урок 2. Первый треугольник
  • Урок 3. Матрицы
  • Урок 4. Цветной куб
  • Урок 5. Текстурированный куб
  • Урок 6. Клавиатура и мышь
  • Урок 7. Загрузка моделей
  • Урок 8. Базовый шейдинг

Продвинутые уроки:

  • Урок 9. VBO индексация
  • Урок 10. Прозрачность
  • Урок 11. 2D текст
  • Урок 12. OpenGL расширения
  • Урок 13. Normal Mapping
  • Урок 14. Отрисовка на текстуру
  • Урок 15. Lightmaps
  • Урок 16. Shadow mapping
  • Урок 17. Вращение
  • Урок 18.1. «Билборды»
  • Урок 18.2. Частицы

Всякое:

  • Урок 19. FPS счетчик
  • Урок 20.1. Нажатие на объекты с помощью OpenGL хака
  • Урок 20.2. Нажатие на объекты с помощью физического движка
  • Урок 20.3. Нажатие на объекты с помощью собственного raycastingа

Статья

Вступление

Добро пожаловать в первый урок.

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

Требования

Никаких особых требований для этих уроков не требуется. Желательно иметь опыт работы с любым языком программирования (C, Java, Lisp, Javascript и т.д.), для того, что бы полностью понимать код, но это не обязательно. Просто будет сложнее учить сразу 2 вещи.

Все уроки написаны на «Легком С++»: Множество усилий было приложено для того, что бы сделать код максимально простым. Нет шаблонов, нет классов, нет указателей. Так Вы сможете понять все, даже если знаете только Java.

Забудьте все

Вам не нужно ничего знать, но если Вы что-то знаете про OpenGL, забудьте это. Если Вы знаете что-то про glBegin(), забудьте это. Здесь Вы будете изучать современный OpenGL (OpenGL 3 и 4), а большинство уроков, которые Вы можете найти в интернете — по старому OpenGL (OpenGL 1 и 2).
Так что забудьте все, что Вы можете знать про OpenGL, иначе у вас мозг вскипит от смешивания разных стандартов.

Сборка

Все уроки могут быть собраны на Windows, Linux и Mac. Для всех этих платформ, процедура одна и та же:

  • Обновите драйвера. Вас предупредили
  • Скачайте компилятор, если у вас еще нет его
  • Установите CMake
  • Скачайте исходный код уроков
  • Сгенерируйте проект с помощью CMake
  • Соберите его
  • Поиграйтесь с примерами!

Детальнее, алгоритмы сборки под разные платформы представлены ниже:

Сборка под Windows

  • Обновить драйвера должно быть просто. Просто скачайте драйвера с сайта AMD или NVidia, в зависимости от вашей видеокарты. Если вы не уверены в том, GPU какой компании у вас стоит: Панель управления -> Система и Безопасность -> Система -> Диспетчер устройств -> Видеочип. Если у вас интегрированная Intel видеокарта — то драйвера зачастую предоставляются производителем (Dell, HP и т.д.)
  • Мы советуем использовать Visual Studio 2015 Express for Desktop в качестве компилятора. Вы можете скачать его бесплатно отсюда. Если Вы предпочитаете MinGW, тогда рекомендуем использовать QtCreator. Устанавливайте ПО в зависимости от ваших предпочтений. Дальнейшее указания будут даны для Visual Studio. Адаптируйте их для вашей IDE.
  • Скачайте CMake и установите его.
  • Скачайте исходный код и разархивирейте его к примеру в C:\Users\XYZ\Projects\OpenGLTutorials\
  • Запустите CMake. В первой строке введите путь до папки с исходниками. В указанной Вами папке должен находиться CMakeLists.txt. Во второй строке укажите путь, в который будут сохранены результаты работы CMake.
  • Нажмите на кнопку «Configure». Поскольку это первая настройка проекта, CMake спросит, какой компилятор Вы хотите использовать. Выбирайте мудро, основываясь на первом шаге. Если у вас 64 битная версия Windows, Вы можете выбрать 64 битный компилятор. Если Вы не уверены, выбирайте 32 битный.
  • Нажимайте на кнопку «Configure» до тех пор, пока все красные строки не пропадут. Далее нажмите на кнопку «Generate». Проект Visual Studio создан. Теперь Вы можете забыть про CMake.
  • Откройте папку, которую Вы указали во второй строке. Найдите там файл Tutorials.sln. Откройте его с помощью Visual Studio.
  • В меню «Build» нажмите «Build All». Все уроки и зависимости скомпилированы. Все исполняемые файлы были скопированы в папку с уроком. Надеюсь ошибок не возникло.
  • Откройте файл playground.exe и появится черное окно.

Вы также можете запустить каждый урок прямо из Visual Studio. Для этого нажмите правой кнопкой мыши на уроке, выберите «Choose as startup project». Теперь можете отлаживать его с помощью F5.

Сборка под Linux

Существует очень большое количество различных дистрибутивов Linux и просто невозможно описать процесс сборки для каждой конкретной платформы. Вот общая процедура. Адаптируйте ее для своего дистрибутива:

  • Установите последние драйвера. Мы крайне рекомендуем бинарные драйвера с закрытым исходным кодом. Они не открытые, но они работают. Если ваш дистрибутив не предоставляет автоматическую установку, попробуйте урок с сайта Ubuntu.
  • Установите необходимые компиляторы, инструменты и библиотеки. Полный список: cmake make g++ libx11-dev libxi-dev libgl1-mesa-dev libglu1-mesa-dev libxrandr-dev libxext-dev libxi-dev. или
  • Скачайте исходный код и разархивируйте его.
  • Перейдите в папку с проектом и выполните следующие команды:
  • mkdir build
  • cd build
  • cmake ..
  • Makefile был создан в build папке.
  • Команда «make all» соберет все уроки и их зависимости. Все исполняемые файлы будут скопированы в папку с проектом.
  • Запустите файла ./playground. Должно появиться черное окно.

Заметьте, что Вы также можете использовать IDE, вроде QtCreator. Главное, что бы она поддерживала CMake. Вот инструкция для QtCreator:

  • В QtCreator откройте File -> Tools -> Options -> Compile&Execute -> CMake
  • Установите путь до CMake. Зачастую это /usr/bin/cmake
  • File -> Open Project; Выберите tutorials/CMakeLists.txt
  • Выберите папку для сборки. Желательно она должна быть вне папки tutorials.
  • Можете установить флаг -DCMAKE_BUILD_TYPE=Debug в параметрах.
  • Нажмите на Молот внизу. Все уроки будут собраны
  • Для запуска уроков из QtCreator, нажмите на Projects -> Execution parameters -> Working Directory и выберите папку, где находятся шейдеры, текстуры и модели.

Сборка под Mac

Процесс очень схож со сборкой под Windows. (Makefile также поддерживаются, но здесь они не описаны).

  • Установите XCode из Mac App Store
  • Скачайте CMake и установите .dmg файл. Вам не надо устанавливать инструменты коммандной строки
  • Скачайте исходные коды и разархивируйте их
  • Запустите CMake (Applications -> CMake). А первой строке укажите путь до проекта. В этой папке должен находиться файл CMakeLists.txtю Во второй строке укажите путь до папки в которую будут сохранены результаты работы CMake.
  • Нажмите на кнопку «Configure». Поскольку это первая настройка проекта, CMake спросит, какой компилятор Вы хотите использовать. Выберите XCode.
  • Нажимайте на кнопку «Configure» до тех пор, пока не пропадут все красные строки. Нажмите на «Generate». Все, ваш проект XCode создан. Можете забыть про CMake.
  • Откройте папку, путь до которой Вы прописывали во второй строке. Найдите там файл Tutorials.xcodeproj. Откройте его.
  • Выберите какой-нибудь урок и запустите его с помощью кнопки Run

Заметка к Code::Blocks

В связи с 2 багами (один в Code::Blocks, один в CMake), Вам потребуется изменить настройки в Project->Build Options->Make commands следующим образом:

Также Вам придется настроить рабочую директорию самостоятельно: Project -> Properties -> Build targets -> tutorial N -> Рабочая папка.

Запуск уроков

Рабочими директориями всех скомпилированных уроков должны быть папки с исполняемыми файлами.

Как следовать урокам

Каждый урок содержит исходный код и другие файлы, которые могут быть найдены в tutorialXX/ директории. Однако Вы не должны изменять эти проекты, они поставляются только в качестве примеров. Для изменений есть файл playground/playground.cpp. Если что-то пойдет не так в этом файле, просто скопируйте код из требуемого урока.

Открытие окна

Вот мы и добрались до OpenGL кода! Ну то есть не совсем. Все другие уроки показывают низкоуровневый путь для выполнения тех или иных действий, для того, что бы Вы видели, что никакой магии не происходит. Но это скучно и бесполезно, поэтому мы будем использовать стороннюю библиотеку GLFW для предоставления магии. Если Вам очень хочется — Вы можете использовать Win32 API под Windows, X11 API под Linux и Cocoa API под Mac; ну или воспользоваться другой высокоуровневой библиотекой, вроде SFML, FreeGLUT, SDL и т.д.

Что же, начем. В начале разберемся с зависимостями: нам потребуется некоторый базовый функционал для вывода сообщений в консоль:

Далее подключаем GLEW. Эта библиотека предоставляет немного магии, но оставим это на потом:

Мы решили, что будем использовать GLFW для работы с окном и клавиатурой, поэтому подключаем и ее:

Следующая библиотека сейчас нам не понадобится, но она предоставляет функционал для работы с 3D математикой. Очень скоро она нам понадобится. В GLM нет никакой магии, если хотите — можете написать свою собственную библиотеку для работы с 3D математикой. Директива «using namespace» нужна для того, что бы можно было писать просто «vec3» вместо «glm::vec3»:

Если Вы скопировали вышеуказанный код в playground и попытались его запустить — то компилятор Вам сообщит, что нет функции main. Что же, давайте добавим ее:

Для начала инициализируем GLFW:

Теперь мы можем создать наше OpenGL окно!

Соберите этот код и запустите. Должно появиться черное окно и сразу закрыться. Ну конечно! Нам требуется подождать пока пользователь не нажмет Escape:

На этом заканчивается первый урок! Во 2 уроке Вы научитесь рисовать треугольник.

Опыт изучения OpenGL — Часть 1 — Введение

С детства мечтал писать компьютерные игры, и вот, года три назад решил рискнуть — поизучать OpenGL. Почему взялся изучать не игровой движок, а низкоуровневый API (API — Application Programming Interface)? Потому что люблю изучать все с как можно более низкого уровня, чтобы разобраться как оно работает (например, изучение языка ассемблера Intel очень помогло моему пониманию языков Си и C++). А почему OpenGL, а не DirectX? — Просто по OpenGL я смог найти больше литературы, чем по DirectX. Изучать OpenGL — задачка очень непростая, а теперь появился еще более низкоуровневый API — Vulkan, пытаюсь и про него читать понемногу. Но сначала хочу поделиться с вами своим опытом изучения OpenGL. Штурмовал я его раза три c перерывами в год, и на третий раз наконец достиг чего-то более сложного, чем рисование разноцветных треугольников — объемные движущиеся объекты, движущаяся камера, освещение, текстуры и тени — уже, на мой взгляд, неплохо (рисунок 1).

Рисунок 1 — Солнце, Земля и Луна и Млечный Путь на заднем плане, отрисованные при помощи OpenGL. Луна вращается вокруг Земли, а Земля и Солнце — вокруг своих осей. Солнце является единственным точечным источником света. Используется модель освещения Phong lighting model плюс shadow mapping. Всё это отрисовывается программой, которую я написал сам, используя только OpenGL, стандартную библиотеку языка C++ (STL) и WinAPI.

Литература

Книги по компьютерной графике вообще и по OpenGL в частности (указаны в порядке субъективного убывания полезности):

  • Jason McKesson — Learning Modern 3D Graphics Programming
  • David Wolf — OpenGL 4 Shading Language Cookbook (2nd Edition)
  • Jason Gregory — Game Engine Architecture (2nd Edition)
  • Edward Angel, Dave Shreiner — Interactive Computer Graphics (6th Edition)
  • Dave Shreiner, Graham Sellers, John Kessenich, Bill Licea-Kane — OpenGL Programming Guide (8th Edition)
  • John Kessenich, Graham Sellers, Dave Shreiner — OpenGL Programming Guide (9th Edition)
  • Graham Sellers — OpenGL SuperBible (7th Edition)
  • OpenGL 4.5 API and Shading Language Reference Pages

Книги по C++ (все перечисленные, на мой взгляд, одинаково полезны):

  • Бьярн Страуструп — Программирование. Принципы и практика с использованием C++
  • Bjarne Stroustrup. The C++ Programming Language. Fourth Edition.
  • Скотт Мейерс — Эффективное использование C++
  • Скотт Мейерс — Наиболее эффективное использование C++
  • Scott Meyers — Effective Modern C++

OpenGL — это C API, т. е. пользоваться им проще всего в программах на языках C/C++. Авторы книг ставят перед собой цель продемонстрировать непосредственное использование этого API, поэтому в книгах по OpenGL весь код как правило написан на языке Си, причем он длинный, трудночитаемый и трудноизменяемый. Я хотел сравнительно легко и быстро писать графические программы, поэтому должен был написать свой API поверх OpenGL, и конечно, на C++ (далее в тексте я, говоря о своем проекте, могу называть его по-разному: API, движок, фреймворк). Когда создаешь свой API, хорошо бывает применить top-down approach, т. е. проектирование «сверху вниз». Я этот подход описываю так: представьте, как должна выглядеть ваша программа, когда ваш API уже полностью написан. Запишите эту программу — в ней будут конечно же создаваться объекты, вызываться методы классов… Вот, теперь вы знаете какие классы вам нужны и какие у них должны быть методы — напишите же их!

В написании собственного API мне очень помогла великолепная книжка [Gregory]. Что касается книг по API OpenGL… Официальное руководство по программированию (т. н. красная книга) [OpenGL Programming Guide], как мне кажется, не очень годится для изучения, оно скорее напоминает справочник, и читать его ужасно скучно. К счастью я нашел минимум две книги «с человеческим лицом». [McKesson] хорошо объясняет основы компьютерной графики (графический конвейер, камера, перспективная проекция, матрицы, шейдеры, освещение, текстуры) — эту книгу приятно и интересно читать (правда в ней используется OpenGL 3.3, и некоторые функции, используемые в книге, можно уже заменить на более новые из OpenGL 4). Книжка [Wolf] — это собрание практических примеров реализации той или иной техники создания визуальных эффектов (освещение, текстурирование, тени, blending и пр.), написанное очень понятным языком.

Надо заметить, что практически во всех книгах авторы пользуются вспомогательными библиотеками для работы с оконным интерфейсом операционной системы — чаще всего это библиотеки FreeGLUT или GLFW. Для векторных вычислений используют библиотеку GLM. Я же принципиально не использовал никакие библиотеки (библиотека GLEW не в счет — она лишь загружает функции OpenGL), поскольку хотел освоить буквально все аспекты программирования графических приложений с нуля. Поэтому в дальнейших заметках речь пойдет, помимо прочего, и о создании окон при помощи WinAPI, и о векторных вычислениях.

Создание проекта

Я создал проект в MS Visual Studio 2015, назвал его RenderingEngine. Проект размещен в открытом репозитории на сайте BitBucket (о том как клонировать репозиторий и прочее см. заметку про Git). В этой и последующих заметках я буду приводить фрагменты исходного кода, которые существенны для обсуждения. Привести весь исходный код, прокомментировав каждую строчку невозможно, да и смысла нет. Если вам понадобятся подробности кода, которых нет в заметках, загляните в репозиторий.

Прежде всего я хотел «организовать рабочее пространство» — разложить всё по папочкам. Почитал некоторые рекомендации в интернете (StackOverflow — VC2010 C++ — organizing source files) и решил создать четыре папки:

ПРИМЕЧАНИЕ

Запомните: все преобразования объектов и камеры в OpenGL производятся с помощью умножения векторов координат на матрицы. Причем умножение происходит на текущую матрицу в момент определения координаты командой glVertex* и некоторыми другими.

Модельно-Видовые преобразования

К модельно-видовым преобразованиям будем относить перенос, поворот и изменение масштаба вдоль координатных осей. Для проведения этих операций достаточно умножить на соответствующую матрицу каждую вершину объекта и получить измененные координаты этой вершины:

(x’, y’, z’, 1) T = M * (x, y, z, 1) T

где M – матрица модельно-видового преобразования. Перспективное преобразование и проектирование производится аналогично. Сама матрица может быть создана с помощью следующих команд:

glTranlsate*() производит перенос объекта, прибавляя к координатам его вершин значения своих параметров.

glRotate*( ) производит поворот объекта против часовой стрелки на угол angle (измеряется в градусах) вокруг вектора (x,y,z).

glScale*() производит масштабирование объекта (сжатие или растяжение) вдоль вектора (x,y,z), умножая соответствующие координаты его вершин на значения своих параметров.

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

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

где точка ( eyex,eyey,eyez ) определяет точку наблюдения, ( centerx, centery, centerz ) задает центр сцены, который будет проектироваться в центр области вывода, а вектор ( upx,upy,upz ) задает положительное направление оси у, определяя поворот камеры. Если, например, камеру не надо поворачивать, то задается значение (0,1,0), а со значением (0,-1,0) сцена будет перевернута.

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

Запомните: В общем случае матричные преобразования в OpenGL нужно записывать в обратном порядке. Например, если вы хотите сначала повернуть объект, а затем передвинуть его, сначала вызовите команду glTranslate(), а только потом – glRotate(). Ну а после этого определяйте сам объект.

Проекции

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

Рис. 6 Ортографическая проекция

Первая команда создает матрицу проекции в усеченный объем видимости (параллелепипед видимости) в левосторонней системе координат. Параметры команды задают точки ( left, bottom, znear ) и ( right, top, zfar ), которые отвечают левому нижнему и правому верхнему углам окна вывода. Параметры near и far задают расстояние до ближней и дальней плоскостей отсечения по удалению от точки (0,0,0) и могут быть отрицательными.

Во второй команде, в отличие от первой, значения near и far устанавливаются равными –1 и 1 соответственно. Это удобно, если OpenGL используется для рисования двумерных объектов. В этом случае положение вершин можно задавать, используя команды glVertex2*()

Перспективная проекция определяется командой

которая задает усеченный конус видимости в левосторонней системе координат. Параметр angley определяет угол видимости в градусах по оси у и должен находиться в диапазоне от 0 до 180. Угол видимости вдоль оси x задается параметром aspect , который обычно задается как отношение сторон области вывода (как правило, размеров окна) Параметры zfar и znear задают расстояние от наблюдателя до плоскостей отсечения по глубине и должны быть положительными. Чем больше отношение zfar / znear , тем хуже в буфере глубины будут различаться расположенные рядом поверхности, так как по умолчанию в него будет записываться ‘сжатая’ глубина в диапазоне от 0 до 1 (см. п. 0.).

Рис. 7 Перспективная проекция

Прежде чем задавать матрицы проекций, не забудьте включить режим работы с нужной матрицей командой glMatrixMode(GL_PROJECTION ) и сбросить текущую, вызвав glLoadIdentity().

Область вывода

После применения матрицы проекций на вход следующего преобразования подаются так называемые усеченные (clipped) координаты. Затем находятся нормализованные координаты вершин по формуле:

(x n , y n , z n ) T = (x c /w c , y c /w c , z c /w c ) T

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

Значения всех параметров задаются в пикселах и определяют ширину и высоту области вывода с координатами левого нижнего угла ( x , y ) в оконной системе координат. Размеры оконной системы координат определяются текущими размерами окна приложения, точка (0,0) находится в левом нижнем углу окна.

Используя параметры команды glViewPort() , OpenGL вычисляет оконные координаты центра области вывода (o x ,o y ) по формулам o x =x+width/2, o y =y+height/2.

Пусть p x =width, p y =height, тогда можно найти оконные координаты каждой вершины:

(x w , y w , z w ) T = ( (p x /2) x n + o x , (p y /2) y n + o y , [(f-n)/2] z n +(n+f)/2 ) T

При этом целые положительные величины n и f задают минимальную и максимальную глубину точки в окне и по умолчанию равны 0 и 1 соответственно. Глубина каждой точки записывается в специальный буфер глубины (z-буфер), который используется для удаления невидимых линий и поверхностей. Установить значения n и f можно вызовом функции

Команда glViewPort() обычно используется в функции, зарегистрированной с помощью команды glutReshapeFunc(), которая вызывается, если пользователь изменяет размеры окна приложения.

Контрольные вопросы

Какие системы координат используются в OpenGL?

Перечислите виды матричных преобразований в OpenGL. Каким образом происходят преобразования объектов в OpenGL?

Что такое матричный стек?

Перечислите способы изменения положения наблюдателя в OpenGL.

Какая последовательность вызовов команд glTranslate(), glRotate() и glScale() соответствует команде gluLookAt(0, 0, -10, 10, 0, 0, 0, -1, 0)?

Какие вы знаете стандартные команды для задания проекций?

Что такое видовые координаты? Нормализованные координаты?

Как с помощью OpenGL задать косоугольную проекцию?

Материалы и освещение

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

Модель освещения

В OpenGL используется модель освещения, в соответствии с которой цвет точки определяется несколькими факторами: свойствами материала и текстуры, величиной нормали в этой точке, а также положением источника света и наблюдателя. Для корректного расчета освещенности в точке надо использовать единичные нормали, однако команды типа glScale*(), могут изменять длину нормалей. Чтобы это учитывать, используйте уже упоминавшийся в пункте 0 режим нормализации векторов нормалей, который включается вызовом команды glEnable(GL_NORMALIZE).

Для задания глобальных параметров освещения используются команды

Аргумент pname определяет, какой параметр модели освещения будет настраиваться и может принимать следующие значения:

GL_LIGHT_MODEL_LOCAL_VIEWER параметр param должен быть булевским и задает положение наблюдателя. Если он равен FALSE, то направление обзора считается параллельным оси –z, вне зависимости от положения в видовых координатах. Если же он равен TRUE, то наблюдатель находится в начале видовой системы координат. Это может улучшить качество освещения, но усложняет его расчет. Значение по умолчанию: FALSE.
GL_LIGHT_MODEL_TWO_SIDE параметр param должен быть булевским и управляет режимом расчета освещенности как для лицевых, так и для обратных граней. Если он равен FALSE, то освещенность рассчитывается только для лицевых граней. Если же он равен TRUE, расчет проводится и для обратных граней. Значение по умолчанию: FALSE.
GL_LIGHT_MODEL_AMBIENT параметр params должен содержать четыре целых или вещественных числа, которые определяют цвет фонового освещения даже в случае отсутствия определенных источников света. Значение по умолчанию: (0.2, 0.2, 0.2,1.0).

Спецификация материалов

Для задания параметров текущего материала используются команды

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

GL_AMBIENT параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют рассеянный цвет материала (цвет материала в тени). Значение по умолчанию: (0.2, 0.2, 0.2, 1.0).
GL_DIFFUSE параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет диффузного отражения материала. Значение по умолчанию: (0.8, 0.8, 0.8, 1.0).
GL_SPECULAR параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет зеркального отражения материала. Значение по умолчанию: (0.0, 0.0, 0.0, 1.0).
GL_SHININESS параметр params должен содержать одно целое или вещественное значение в диапазоне от 0 до 128, которое определяет степень зеркального отражения материала. Значение по умолчанию: 0.
GL_EMISSION параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют интенсивность излучаемого света материала. Значение по умолчанию: (0.0, 0.0, 0.0, 1.0).
GL_AMBIENT_AND_DIFFUSE эквивалентно двум вызовам команды glMaterial*() со значением pname GL_AMBIENT и GL_DIFFUSE и одинаковыми значениями params.

Из этого следует, что вызов команды glMaterial[i f]() возможен только для установки степени зеркального отражения материала (shininess). Команда glMaterial[i f]v() используется для задания остальных параметров.

Параметр face определяет тип граней, для которых задается этот материал и может принимать значения GL_FRONT , GL_BACK или GL_FRONT_AND_BACK .

Если в сцене материалы объектов различаются лишь одним параметром, рекомендуется сначала установить нужный режим, вызвав glEnable() c параметром GL_COLOR_MATERIAL , а затем использовать команду

где параметр face имеет аналогичный смысл, а параметр pname может принимать все перечисленные значения. После этого значения выбранного с помощью pname свойства материала для конкретного объекта (или вершины) устанавливаются вызовом команды glColor*(), что позволяет избежать вызовов более ресурсоемкой команды glMaterial*() и повышает эффективность программы. Другие методы оптимизации приведены в п. 0.

Пример определения свойств материала:

Описание источников света

Определение свойств материала объекта имеет смысл, только если в сцене есть источники света. Иначе все объекты будут черными (или, строго говоря, иметь цвет, равный рассеянному цвету материала, умноженному на интенсивность глобального фонового освещения, см. команду glLightModel). Добавить в сцену источник света можно с помощью команд

Параметр light однозначно определяет источник света. Он выбирается из набора специальных символических имен вида GL_LIGHTi , где i должно лежать в диапазоне от 0 до константы GL_MAX_LIGHT , которая обычно не превосходит восьми.

Параметры pname и params имеют смысл, аналогичный команде glMaterial*(). Рассмотрим значения параметра pname :

GL_SPOT_EXPONENT параметр param должен содержать целое или вещественное число от 0 до 128, задающее распределение интенсивности света. Этот параметр описывает уровень сфокусированности источника света. Значение по умолчанию: 0 (рассеянный свет).
GL_SPOT_CUTOFF параметр param должен содержать целое или вещественное число между 0 и 90 или равное 180, которое определяет максимальный угол разброса света. Значение этого параметра есть половина угла в вершине конусовидного светового потока, создаваемого источником. Значение по умолчанию: 180 (рассеянный свет).
GL_AMBIENT параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет фонового освещения. Значение по умолчанию: (0.0, 0.0, 0.0, 1.0).
GL_DIFFUSE параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет диффузного освещения. Значение по умолчанию: (1.0, 1.0, 1.0, 1.0) для LIGHT0 и (0.0, 0.0, 0.0, 1.0) для остальных.
GL_SPECULAR параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет зеркального отражения. Значение по умолчанию: (1.0, 1.0, 1.0, 1.0) для LIGHT0 и (0.0, 0.0, 0.0, 1.0) для остальных.
GL_POSITION параметр params должен содержать четыре целых или вещественных числа, которые определяют положение источника света. Если значение компоненты w равно 0.0, то источник считается бесконечно удаленным и при расчете освещенности учитывается только направление на точку (x,y,z), в противном случае считается, что источник расположен в точке (x,y,z,w). В первом случае ослабления света при удалении от источника не происходит, т.е. источник считается бесконечно удаленным. Значение по умолчанию: (0.0, 0.0, 1.0, 0.0).
GL_SPOT_DIRECTION параметр params должен содержать четыре целых или вещественных числа, которые определяют направление света. Значение по умолчанию: (0.0, 0.0, -1.0, 1.0). Эта характеристика источника имеет смысл, если значение GL_SPOT_CUTOFF отлично от 180 (которое, кстати, задано по умолчанию).
GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION параметр params задает значение одного из трех коэффициентов, определяющих ослабление интенсивности света при удалении от источника. Допускаются только неотрицательные значения. Если источник не является направленным (см. GL_POSITION ), то ослабление обратно пропорционально сумме: att constant +att linear *d+ att quadratic *d 2 , где d – расстояние между источником света и освещаемой им вершиной, att constant , att linear и att quadratic равны параметрам, заданным с помощью констант GL_CONSTANT_ATTENUATION , GL_LINEAR_ATTENUATION и GL_QUADRATIC_ATTENUATION соответственно. По умолчанию эти параметры задаются тройкой (1, 0, 0), и фактически ослабления не происходит.

При изменении положения источника света следует учитывать следующий факт: в OpenGL источники света являются объектами, во многом такими же, как многоугольники и точки. На них распространяется основное правило обработки координат в OpenGL – параметры, описывающее положение в пространстве, преобразуются текущей модельно-видовой матрицей в момент формирования объекта, т.е. в момент вызова соответствующих команд OpenGL. Таким образом, формируя источник света одновременно с объектом сцены или камерой, его можно привязать к этому объекту. Или, наоборот, сформировать стационарный источник света, который будет оставаться на месте, пока другие объекты перемещаются.

Общее правило такое:

Если положение источника света задается командой glLight*() перед определением положения виртуальной камеры (например, командой glLookAt()), то будет считаться, что координаты (0,0,0) источника находится в точке наблюдения и, следовательно, положение источника света определяется относительно положения наблюдателя.

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

Для использования освещения сначала надо установить соответствующий режим вызовом команды glEnable(GL_LIGHTNING), а затем включить нужный источник командой glEnable(GL_LIGHTi).

Еще раз обратим внимание на то, что при выключенном освещении цвет вершины равен текущему цвету, который задается командами glColor*(). При включенном освещении цвет вершины вычисляется исходя из информации о материале, нормалях и источниках света.

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

Текст программы, демонстрирующей основные принципы определения материалов и источников света, приведен в приложении 0

Создание эффекта тумана

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

Туман в OpenGL реализуется путем изменения цвета объектов в сцене в зависимости от их глубины, т.е. расстояния до точки наблюдения. Изменение цвета происходит либо для вершин примитивов, либо для каждого пикселя на этапе растеризации в зависимости от реализации OpenGL. Этим процессом можно частично управлять – см. раздел 0.

Для включения эффекта затуманивания необходимо вызвать команду glEnable(GL_FOG) .

Метод вычисления интенсивности тумана в вершине можно определить с помощью команд

Аргумент pname может принимать следующие значения:

GL_FOG_MODE аргумент param определяет формулу, по которой будет вычисляться интенсивность тумана в точке.

В этом случае param может принимать значения

GL_EXP Интенсивность вычисляется по формуле f=exp(-d*z)
GL_EXP2 Интенсивность вычисляется по формуле f=exp(-(d*z) 2 )
GL_LINEAR Интенсивность вычисляется по формуле f=e-z/e-sгде z – расстояние от вершины, в которой вычисляется интенсивность тумана, до точки наблюдения.Коэффициенты d,e,s задаются с помощью следующих значений аргумента pname
GL_FOG_DENSITY param определяет коээфициент d
GL_FOG_START param определяет коэффициент s
GL_FOG_END param определяет коэффициент e

Цвет тумана задается с помощью аргумента pname , равного

GL_FOG_COLOR params – указатель на массив из 4-х компонент цвета

Приведем пример использования этого эффекта:

Контрольные вопросы

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

Для чего служит команда glColorMaterial?

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

Как задать фиксированное положение источника света? Можно ли задавать положение источника относительно локальных координат объекта?

Как задать конусный источник света?

Если в сцене включено освещение, но нет источников света, какой цвет будут иметь объекты?

Текстурирование

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

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

Для работы с текстурой следует выполнить следующую последовательность действий:

выбрать изображение и преобразовать его к нужному формату;

передать изображение в OpenGL;

определить, как текстура будет наноситься на объект и как она будет с ним взаимодействовать;

связать текстуру с объектом.

Подготовка текстуры

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

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

где file – название файла с расширением *.bmp или *.dib. Функция возвращает указатель на область памяти, где хранятся преобразованные данные.

При создании образа текстуры в памяти следует учитывать следующие требования:

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

В качестве значения параметра format обычно используется значение GL_RGB или GL_RGBA , определяющее формат хранения информации. Параметры widthin , heightin , widhtout , heightout определяют размеры входного и выходного изображений, а с помощью typein и typeout задается тип элементов массивов, расположенных по адресам datain и dataout . Как и обычно, это может быть тип GL_UNSIGNED_BYTE, GL_SHORT, GL_INT и так далее. Результат своей работы функция заносит в область памяти, на которую указывает параметр dataout .

Во-вторых, надо предусмотреть случай, когда объект после растеризации оказывается по размерам значительно меньше наносимой на него текстуры. Чем меньше объект, тем меньше должна быть наносимая на него текстура и поэтому вводится понятие уровней детализации текстуры . (mipmap) Каждый уровень детализации задает некоторое изображение, которое является, как правило, уменьшенной в два раза копией оригинала. Такой подход позволяет улучшить качество нанесения текстуры на объект. Например, для изображения размером 2 m x2 n можно построить max(m,n)+1 уменьшенных изображений, соответствующих различным уровням детализации.

Эти два этапа создания образа текстуры во внутренней памяти OpenGL можно провести с помощью команды


где параметр target должен быть равен GL_TEXTURE_2D. Параметр components определяет количество цветовых компонент текстуры и может принимать следующие основные значения:

GL_LUMINANCE одна компонента – яркость. (текстура будет монохромной)
GL_RGB красный, синий, зеленый
GL_RGBA все компоненты

Параметры width , height , data определяют размеры и расположение текстуры соответственно, а format и type имеют аналогичный смысл, что и в команде gluScaleImage().

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

В OpenGL допускается использование одномерных текстур, то есть размера 1xN, однако, это всегда надо указывать, задавая в качестве значения target константу GL_TEXTURE_1D . Полезность одномерных текстур сомнительна, поэтому не будем останавливаться на этом подробно.

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

надо создать n идентификаторов текстур, которые будут записаны в массив textures . Перед началом определения свойств очередной текстуры следует сделать ее текущей («привязать» текстуру), вызвав команду

где target может принимать значения GL_TEXTURE_1D или GL_TEXTURE_2D , а параметр texture должен быть равен идентификатору той текстуры, к которой будут относиться последующие команды. Для того, чтобы в процессе рисования сделать текущей текстуру с некоторым идентификатором, достаточно опять вызвать команду glBindTexture() c соответствующим значением target и texture . Таким образом, команда glBindTexture() включает режим создания текстуры с идентификатором texture , если такая текстура еще не создана, либо режим ее использования, то есть делает эту текстуру текущей.

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

Наложение текстуры на объекты

При наложении текстуры, как уже упоминалось, надо учитывать случай, когда размеры текстуры отличаются от оконных размеров объекта, на который она накладывается. При этом возможно как растяжение, так и сжатие изображения, и то, как будут проводиться эти преобразования, может серьезно повлиять на качество построенного изображения. Для определения положения точки на текстуре используется параметрическая система координат (s,t), причем значения s и t находятся в отрезке [0,1] (см. рисунок)

Рис. 8 Текстурные координаты

Для изменения различных параметров текстуры применяются команды:

При этом target может принимать значения GL_TEXTURE_1D или GL_TEXTURE_2D , pname определяет, какое свойство будем менять, а с помощью param или params устанавливается новое значение. Возможные значения pname :

GL_TEXTURE_MIN_FILTER параметр param определяет функцию, которая будет использоваться для сжатия текстуры. При значении GL_NEAREST будет использоваться один (ближайший), а при значении GL_LINEAR четыре ближайших элемента текстуры. Значение по умолчанию: GL_LINEAR
GL_TEXTURE_MAG_FILTER параметр param определяет функцию, которая будет использоваться для увеличения (растяжения) текстуры. При значении GL_NEAREST будет использоваться один (ближайший), а при значении GL_LINEAR четыре ближайших элемента текстуры. Значение по умолчанию: GL_LINEAR
GL_TEXTURE_WRAP_S параметр param устанавливает значение координаты s, если оно не входит в отрезок [0,1]. При значении GL_ REPEAT целая часть s отбрасывается, и в результате изображение размножается по поверхности. При значении GL_CLAMP используются краевые значения: 0 или 1, что удобно использовать, если на объект накладывается один образ. Значение по умолчанию: GL_REPEAT
GL_TEXTURE_WRAP_T аналогично предыдущему значению, только для координаты t

Использование режима GL_NEAREST повышает скорость наложения текстуры, однако при этом снижается качество, так как в отличие от GL_LINEAR интерполяция не производится.

Для того чтобы определить, как текстура будет взаимодействовать с материалом, из которого сделан объект, используются команды

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

Наиболее часто используемые значения параметра param :

GL_MODULATE конечный цвет находится как произведение цвета точки на поверхности и цвета соответствующей ей точки на текстуре.
GL_REPLACE в качестве конечного цвета используется цвет точки на текстуре.

Следующая программа демонстрирует общий подход к созданию текстур:

Текстурные координаты

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

Первый метод реализуется с помощью команд

Чаще всего используется команды вида glTexCoord2*(type s, type t) , задающие текущие координаты текстуры. Понятие текущих координат текстуры аналогично понятиям текущего цвета и текущей нормали, и является атрибутом вершины. Однако даже для куба нахождение соответствующих координат текстуры является довольно трудоемким занятием, поэтому в библиотеке GLU помимо команд, проводящих построение таких примитивов, как сфера, цилиндр и диск, предусмотрено также наложение на них текстур. Для этого достаточно вызвать команду

с параметром textureCoords равным GL_TRUE , и тогда текущая текстура будет автоматически накладываться на примитив.

Второй метод реализуется с помощью команд

Параметр coord определяет, для какой координаты задается формула, и может принимать значение GL_S,GL_T ; pname может быть равен одному из следующих значений:

GL_TEXTURE_GEN_MODE определяет функцию для наложения текстуры.

В этом случае аргумент param принимает значения:

GL_OBJECT_LINEAR значение соответствующей текстурной координаты определяется расстоянием до плоскости, задаваемой с помощью значения pname GL_OBJECT_PLANE (см. ниже). Формула выглядит следующим образом: g=x*xp+y*yp+z*zp+w*wp, где g-соответствующая текстурная координата ( s или p), x, y, z, w – координаты соответствующей точки. xp, yp, zp, wp – коэффициенты уравнения плоскости. В формуле используются координаты объекта.
GL_EYE_LINEAR аналогично предыдущему значению, только в формуле используются видовые координаты. Т.е. координаты текстуры объекта в этом случае зависят от положения этого объекта.
GL_SPHERE_MAP позволяет эмулировать отражение от поверхности объекта. Текстура как бы «оборачивается» вокруг объекта. Для данного метода используются видовые координаты и необходимо задание нормалей.
GL_OBJECT_PLANE позволяет задать плоскость, расстояние до которой будет использоваться при генерации координат, если установлен режим GL_OBJECT_LINEAR. В этом случае параметр params является указателем на массив из четырех коэффициентов уравнения плоскости.
GL_EYE_PLANE аналогично предыдущему значению. Позволяет задать плоскость для режима GL_EYE_LINEAR

Для установки автоматического режима задания текстурных координат необходимо вызвать команду glEnable с параметром GL_TEXTURE_GEN_S или GL_TEXTURE_GEN_P .

Рассмотрим, как можно задать зеркальную текстуру. При таком наложении текстуры изображение будет как бы отражаться от поверхности объекта, вызывая интересный оптический эффект. Для этого сначала надо создать два целочисленных массива коэффициентов s_coeffs и t_coeffs со значениями (1,0,0,1) и (0,1,0,1) соответственно, а затем вызвать команды:

и такие же команды для координаты t с соответствующими изменениями.

Программа, использующая наложение текстуры и анимацию, приведена в приложении 0

Контрольные вопросы

Что такое текстура и для чего используются текстуры?

Что такое текстурные координаты и как задать их для объекта?

Какой метод взаимодействия с материалом (GL_MODULATE, GL_REPLACE) нужно использовать, если текстура представляет собой картину, висящую на стене?

Перечислите известные вам методы генерации текстурных координат в OpenGL.

Для чего используются уровни детализации текстуры (mip-mapping)?

Что такое режимы фильтрации текстуры и как задать их в OpenGL?

Операции с пикселями

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

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

Графическая библиотека OpenGL поддерживает работу со следующими буферами:

несколько буферов цвета

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

Буфер глубины используется для удаления невидимых поверхностей и прямая работа с ним требуется крайне редко.

Буфер-накопитель можно применять для различных операций. Более подробно работа с ним описана в разделе 0.

Буфер маски используется для формирования пиксельных масок (трафаретов), служащих для вырезания из общего массива тех пикселей, которые следует вывести на экран. Буфер маски и работа с ним более подробно рассмотрены в разделах 0, 0 и 0.

Смешивание изображений. Прозрачность

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

Прозрачность реализуется с помощью специального режима смешения цветов (blending). Алгоритм смешения комбинирует цвета так называемых входящих пикселей (т.е. «кандидатов» на помещение в буфер кадра) с цветами соответствующих пикселей, уже хранящихся в буфере. Для смешения используется четвертая компонента цвета – альфа-компонента, поэтому этот режим называют еще альфа-смешиванием. Программа может управлять интенсивностью альфа-компоненты точно так же, как и интенсивностью основных цветов, т.е. задавать значение интенсивности для каждого пикселя или каждой вершины примитива.

Режим включается с помощью команды glEnable(GL_BLEND).

Определить параметры смешения можно с помощью команды:

Параметр src определяет, как получить коэффициент k 1 исходного цвета пикселя, a dst задает способ получения коэффициента k 2 для цвета в буфере кадра. Для получения результирующего цвета используется следующая формула: res=с src *k 1 +c dst *k 2 , где с src – цвет исходного пикселя, c dst – цвет пикселя в буфере кадра (res, k 1 , k 1 , с src, c dst – четырехкомпонентные RGBA-векторы).

Приведем наиболее часто используемые значения агрументов src и dst.

GL_SRC_ALPHA k=(A s ,A s ,A s ,A s )
GL_SRC_ONE_MINUS_ALPHA k=(1,1,1,1)-(A s ,A s ,A s ,A s )
GL_DST_COLOR k=(R d ,G d ,B d )
GL_ONE_MINUS_DST_COLOR k=(1,1,1,1)- (R d ,G d ,B d ,А d )
GL_DST_ALPHA k=(A d ,A d ,A d ,A d )
GL_DST_ONE_MINUS_ALPHA k=(1,1,1,1)-(A d ,A d ,A d ,A d )
GL_SRC_COLOR k=(R s ,G s ,B s )
GL_ONE_MINUS_SRC_COLOR k=(1,1,1,1)- (R s ,G s ,B s ,A s )

Предположим, мы хотим реализовать вывод прозрачных объектов. Коэффициент прозрачности задается альфа-компонентой цвета. Пусть 1 – непрозрачный объект; 0 – абсолютно прозрачный, т.е. невидимый. Для реализации служит следующий код:

Например, полупрозрачный треугольник можно задать следующим образом:

Если в сцене есть несколько прозрачных объектов, которые могут перекрывать друг друга, корректный вывод можно гарантировать только в случае выполнения следующих условий:

Все прозрачные объекты выводятся после непрозрачных.

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

В OpenGL команды обрабатываются в порядке их поступления, поэтому для реализации перечисленных требований достаточно расставить в соответствующем порядке вызовы команд glVertex*(), но и это в общем случае нетривиально.

Буфер-накопитель

Буфер-накопитель ( accumulation buffer ) – это один из дополнительных буферов OpenGL. В нем можно сохранять визуализированное изображение, применяя при этом попиксельно специальные операции. Буфер-накопитель широко используется для создания различных спецэффектов.

Изображение берется из буфера, выбранного на чтение командой

Аргумент buf определяет буфер для чтения. Значения buf , равные GL_BACK , GL_FRONT , определяют соответствующие буферы цвета для чтения. GL_BACK задает в качестве источника пикселей внеэкранный буфер; GL_FRONT – текущее содержимое окна вывода. Команда имеет значение, если используется дублирующая буферизация. В противном случае используется только один буфер, соответствующий окну вывода (строго говоря, OpenGL имеет набор дополнительных буферов, используемых, в частности, для работы со стереоизображениями, но здесь мы их рассматривать не будем).

Буфер-накопитель является дополнительным буфером цвета. Он не используется непосредственно для вывода образов, но они добавляются в него после вывода в один из буферов цвета. Применяя различные операции, описанные ниже, можно понемногу «накапливать» изображение в буфере.

Затем полученное изображение переносится из буфера-накопителя в один из буферов цвета, выбранный на запись командой

Значение buf аналогично значению соответствующего аргумента в команде glReadBuffer .

Все операции с буфером-накопителем контролируются командой

Аргумент op задает операцию над пикселями и может принимать следующие значения:

GL_LOAD Пиксель выбирается из буфера, выбранного на чтение, его значение умножается на value и заносится в буфер накопления.
GL_ACCUM Аналогично предыдущему, но полученное после умножения значение складывается с уже имеющимся в буфере.
GL_MULT Эта операция умножает значение каждого пикселя в буфере накопления на value .
GL_ADD Аналогично предыдущему, только вместо умножения используется сложение.
GL_RETURN Изображение переносится из буфера накопления в буфер, выбранный для записи. Перед этим значение каждого пикселя умножается на value.

Следует отметить, что для использования буфера-накопителя нет необходимости вызывать какие-либо команды glEnable. Достаточно инициализировать только сам буфер.

Пример использования буфера-накопителя для устранения погрешностей растеризации (ступенчатости) приведен в разделе 0

Буфер маски

При выводе пикселей в буфер кадра иногда возникает необходимость выводить не все пиксели, а только некоторое подмножество, т.е. наложить трафарет (маску) на изображение. Для этого OpenGL предоставляет так называемый буфер маски (stencil buffer). Кроме наложения маски, этот буфер предоставляет еще несколько интересных возможностей.

Прежде чем поместить пиксель в буфер кадра, механизм визуализации OpenGL позволяет выполнить сравнение (тест) между заданным значением и значением в буфере маски. Если тест проходит, пиксель рисуется в буфере кадра.

Механизм сравнения весьма гибок и контролируется следующими командами:

Аргумент ref команды glStencilFunc задает значение для сравнения. Он должен принимать значение от 0 до 2 s –1. s – число бит на точку в буфере маски.

С помощью аргумента func задается функция сравнения. Он может принимать следующие значения:

GL_NEVER тест никогда не проходит, т.е всегда возвращает false
GL_ALWAYS тест проходит всегда.
GL_LESS, GL_LEQUAL, GL_EQUAL, GL_GEQUAL, GL_GREATE, GL_NOTEQUAL тест проходит в случае, если ref соответственно меньше значения в трафаретном буфере, меньше либо равен, равен, больше, больше либо равен или не равен.

Аргумент mask задает маску для значений. Т.е. в итоге для трафаретного теста получаем следующую формулу: (( ref AND mask ) op (svalue AND mask ))

Команда StencilOp предназначена для определения действий над пикселем трафаретного буфера в случае положительного или отрицательного результата теста.

Аргумент sfail задает действие в случае отрицательного результата теста, и может принимать следующие значения:

GL_KEEP, GL_ZERO, GL_REPLACE, GL_INCR, GL_DECR, GL_INVERT соответственно сохраняет значение в трафаретном буфере, обнуляет его, заменяет на заданное значение (ref), увеличивает, уменьшает или побитово инвертирует.

Аргументы dpfail определяют действия в случае отрицательного результата теста на глубину в z-буфере, а dppass задает действие в случае положительного результата этого теста. Аргументы принимают те же значения, что и аргумент sfail . По умолчанию все три параметра установлены на GL_KEEP .

Для включения маскирования необходимо выполнить команду glEnable(GL_STENCIL_TEST);

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

Пример использования буфера маски при построении теней и отражений приведен в разделах 0 и 0.

Управление растеризацией

Способ выполнения растеризации примитивов можно частично регулировать командой glHint (target, mode), где target – вид контролируемых действий, принимает одно из следующих значений

GL_FOG_HINT точность вычислений при наложении тумана. Вычисления могут выполняться по пикселям (наибольшая точность) или только в вершинах. Если реализация OpenGL не поддерживает попиксельного вычисления, то выполняется только вычисление по вершинам
GL_LINE_SMOOTH_HINT управление качеством прямых. При значении mode, равным GL_NICEST , уменьшается ступенчатость прямых за счет большего числа пикселей в прямых
GL_PERSPECTIVE_CORRECTION_HINT точность интерполяции координат при вычислении цветов и наложении текстуры. Если реализация OpenGL не поддерживает режим GL_NICEST , то осуществляется линейная интерполяция координат
GL_POINT_SMOOTH_HINT управление качеством точек. При значении параметра mode равным GL_NICEST точки рисуются как окружности
GL_POLYGON_SMOOTH_HINT управление качеством вывода сторон многоугольника

параметра mode интерпретируется следующим образом:

GL_FASTEST используется наиболее быстрый алгоритм
GL_NICEST используется алгоритм, обеспечивающий лучшее качество
GL_DONT_CARE выбор алгоритма зависит от реализации

Важно заметить, что командой glHint() программист может только определить свои пожелания относительно того или иного аспекта растеризации примитивов. Конкретная реализация OpenGL вправе игнорировать данные установки.

Обратите внимание, что glHint() нельзя вызывать между операторными скобками glBegin()/glEnd().

Контрольные вопросы

Какие буферы изображений используются в OpenGL и для чего?

Для чего используется команда glBlendFunc?

Почему для корректного вывода прозрачных объектов требуется соблюдение условий упорядоченного вывода примитивов с прозрачностью?

Для чего используется буфер-накопитель? Приведите пример работы с ним.

Как в OpenGL можно наложить маску на результирующее изображение?

Объясните, для чего применятся команда glHint(). Каков эффект выполнения команды glHint(GL_FOG_HINT, GL_DONT_CARE)?

Приемы работы с OpenGL

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

Устранение ступенчатости

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

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

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

Все полученные изображения сохраняем в буфере-накопителе с коэффициентом 1/n, где n – число проходов для каждого кадра. Чем больше таких проходов – тем ниже производительность, но лучше результат.

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

Построение теней

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

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

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

Рассмотрим математические основы данного метода.


P – точка в трехмерном пространстве, которая отбрасывает тень.

L – положение источника света, который освещает данную точку.

S =a( L — P )- P — точка, в которую отбрасывает тень точка P, где a – параметр.

Предположим, что тень падает на плоскость z=0. В этом случае a=z p /(z l -z p ). Следовательно,

x s = (x p z l — z l z p ) / (z l — z p ),

y s = (y p z l -y l z p ) / (z l — z p ),

Введем однородные координаты:

Отсюда координаты S могут быть получены с использованием умножения матриц следующим образом:

Для того, чтобы алгоритм мог рассчитывать тень, падающую на произвольную плоскость, рассмотрим произвольную точку на линии между S и P , представленную в однородных координатах:

a P +b L, где a и b – скалярные параметры.

Следующая матрица задает плоскость через координаты ее нормали:

Точка, в которой луч, проведенный от источника света через данную точку P, пересекает плоскость G, определяется параметрами a и b, удовлетворяющими следующему уравнению:

Отсюда получаем: a( PG) + b( LG) = 0. Этому уравнению удовлетворяют

Следовательно, координаты искомой точки S = ( LG)P-(PG)L. Пользуясь ассоциативностью матричного произведения, получим

S = P [( LG ) I — GL ], где I – единичная матрица.

Матрица ( LG ) I — GL используется для получения теней на произвольной плоскости.

Рассмотрим некоторые аспекты практической реализации данного метода с помощью OpenGL.

Предположим, что матрица floorShadow была ранее получена нами из формулы ( LG ) I — GL . Следующий код с ее помощью строит тени для заданной плоскости:

Матрица floorShadow может быть получена из уравнения (*) с помощью следующей функции:

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

Описанный алгоритм предполагает, что плоскости бесконечны, и не отрезает тени по границе. Например, если некоторый объект отбрасывает тень на стол, она не будет отсекаться по границе, и, тем более, «заворачиваться» на боковую поверхность стола.

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

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

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

Имеется решение первой и второй проблемы. Для этого используется буфер маски (см. п. 0)

Итак, задача – отсечь вывод геометрии (тени, в данном случае) по границе некоторой произвольной области и избежать «двойного смешения». Общий алгоритм решения с использованием буфера маски таков:

Очищаем буфер маски значением 0

Отображаем заданную область отсечения, устанавливая значения в буфере маски в 1

Рисуем тени в тех областях, где в буфере маски установлены значения 1. Если тест проходит, устанавливаем в эти области значение 2.

Теперь рассмотрим эти этапы более подробно.

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

Зеркальные отражения

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

Алгоритм использует интуитивное представление полной сцены с зеркалом как составленной из двух: «настоящей» и «виртуальной» – находящейся за зеркалом. Следовательно, процесс рисования отражений состоит из двух частей: 1) визуализации обычной сцены и 2) построения и визуализации виртуальной. Для каждого объекта «настоящей» сцены строится его отраженный двойник, который наблюдатель и увидит в зеркале.

Рис. 9 Зеркальное отражение

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

Упрощенный вариант алгоритма создания плоского отражения состоит из следующих шагов:

Рисуем сцену как обычно, но без объектов-зеркал.

Используя буфер маски, ограничиваем дальнейший вывод проекцией зеркала на экран.

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

Эта последовательность действий позволит получить убедительный эффект отражения.

Рассмотрим этапы более подробно:

Сначала необходимо нарисовать сцену как обычно. Не будем останавливаться на этом этапе подробно. Заметим только, что, очищая буферы OpenGL непосредственно перед рисованием, нужно не забыть очистить буфер маски:

Во время визуализации сцены лучше не рисовать объекты, которые затем станут зеркальными.

На втором этапе необходимо ограничить дальнейший вывод проекцией зеркального объекта на экран.

Для этого настраиваем буфер маски и рисуем зеркало

В результате мы получили:

в буфере кадра – корректно нарисованная сцена, за исключением области зеркала;

в области зеркала (там, где мы хотим видеть отражение) значение буфера маски равно 1.

На третьем этапе нужно нарисовать сцену, отраженную относительно плоскости зеркального объекта.

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

Настраиваем буфер маски на рисование только в областях, где значения буфера равно 1:

и рисуем сцену еще раз (без зеркальных объектов)

Наконец, отключаем маскирование

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

Существует несколько модификаций этого алгоритма, отличающиеся последовательностью действий, ограничениями на геометрию и т.д.

Контрольные вопросы

В результате чего возникает эффект ступенчатости изображения?

Опишите алгоритм устранения ступенчатости.

Почему в OpenGL нет встроенной поддержки построения теней?

Кратко опишите предложенный метод визуализации зеркальных объектов.

Оптимизация программ


Организация приложения

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

Высокоуровневая оптимизация

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

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

Аналогично, объекты, которые располагаются далеко от наблюдателя, могут быть представлены моделями пониженной сложности. Это значительно снизит нагрузку на все ступени конвейера OpenGL. Объекты, которые находятся полностью вне поля видимости, могут быть эффективно отсечены без передачи на конвейер OpenGL с помощью проверки попадания ограничивающих их простых объемов (сфер или кубов) в пирамиду зрения.

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

Низкоуровневая оптимизация

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

Желательно, чтобы использовались структуры данных, которые могут быть быстро и эффективно переданы на конвейер OpenGL. Например, если мы хотим отобразить массив треугольников, то использование указателя на этот массив значительно более эффективно, чем передача его OpenGL поэлементно.

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

Список городов может храниться как массив таких структур. Допустим, мы пишем функцию, которая рисует города на карте в виде точек разного размера с подписями:

Это реализация неудачна по следующим причинам:

glPointSize вызывается для каждой итерации цикла.

между glBegin и glEnd рисуется только одна точка.

вершины определяются в неоптимальном формате.

Ниже приведено более рациональное решение:

В такой реализации мы вызываем glPointSize дважды и увеличиваем число вершин между glBegin и glEnd.

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

Теперь города разных размеров хранятся в разных списках. Положения точек хранятся отдельно в динамическом массиве. После реорганизации мы исключаем необходимость в условном операторе внутри glBegin/glEnd и получаем возможность использовать массивы вершин для оптимизации. В результате наша функция выглядит следующим образом:

Оптимизация вызовов OpenGL

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

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

[А] – предпочтительно для систем с аппаратной поддержкой OpenGL

[П] – предпочтительно для программных реализаций

[все] – вероятно предпочтительно для всех реализаций

Передача данных в OpenGL

В данном разделе рассмотрим способы минимизации времени на передачу данных о примитивах в OpenGL

Используйте связанные примитивы.

Связанные примитивы, такие как GL_LINES , GL_LINE_LOOP , GL_TRIANGLE_STRIP , GL_TRIANGLE_FAN , и GL_QUAD_STRIP требуют для определения меньше вершин, чем отдельные линия или многоугольник. Это уменьшает количество данных, передаваемых OpenGL [все]

Используйте массивы вершин.

На большинстве архитектур замена множественных вызовов glVertex/glColor/glNormal на механизм массивов вершин может быть очень выигрышной. [все]

Используйте индексированные примитивы.

В некоторых случаях даже при использовании связанных примитивов GL_TRIANGLE_STRIP ( GL_QUAD_STRIP ) вершины дублируются.

Чтобы не передавать в OpenGL дубли, увеличивая нагрузку на шину, используйте команду glDrawElements() (см. раздел 0.) [все]

Задавайте необходимые массивы одной командой.

Вместо использования команд

glVertexPointer/glColorPointer/glNormalPointer лучше пользоваться одной командой

так, если имеется структура

то параметры можно передать с помощью следующей команды

что означает, что первые четыре float относятся к цвету, затем три float к нормали, и последние три float задают координаты вершины. Более подробное описание команды смотрите в спецификации OpenGL. [все]

Храните данные о вершинах в памяти последовательно.

Последовательное расположение данных в памяти улучшает скорость обмена между основной памятью и графической подсистемой. [А]

Используйте векторные версии glVertex, glColor, glNormal и glTexCoord.

Функции glVertex*(), glColor*() и т.д., которые в качестве аргументов принимают указатели (например, glVertex3fv(v)) могут работать значительно быстрее, чем их соответствующие версии glVertex3f(x,y,z) [все]

Уменьшайте сложность примитивов.

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

Используйте дисплейные списки.

Используйте дисплейные списки для наиболее часто выводимых объектов. Дисплейные списки могут храниться в памяти графической подсистемы и, следовательно, исключать частые перемещения данных из основной памяти. [А]

Не указывайте ненужные атрибуты вершин

Если освещение выключено, не вызывайте glNormal. Если не используются текстуры, не вызывайте glTexCoord, и т.д. [все]

Минимизируйте количество лишнего кода между glBegin/glEnd

Для максимальной производительности на high-end системах важно, чтобы информация о вершинах была передана графической подсистеме максимально быстро. Избегайте лишнего кода между glBegin/glEnd.

Пример неудачного решения:

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

Преобразования

Преобразования включают в себя трансформации вершин от координат, указанных в glVertex*(), к оконным координатам, отсечение, освещение и т.д.


Избегайте использования локальных источников света, т.е. координаты источника должны быть в форме (x,y,z,0) [П]

Избегайте использования точечных источников света. [все]

Избегайте использования двухстороннего освещения (two-sided lighting). [все]

Избегайте использования отрицательные коэффициентов в параметрах материала и цвета. [П]

Избегайте использования локальной модели освещения .[все]

Избегайте частой смены параметра материала GL_SHININESS [П]

Некоторые реализации OpenGL оптимизированы для случая одного источника света [А, П]

Рассмотрите возможность заранее просчитать освещение. Можно получить эффект освещения, задавая цвета вершин вместо нормалей. [все]

Отключайте нормализацию векторов нормалей, когда это не необходимо.

Команда glEnable/Disable(GL_NORMALIZE) управляет нормализацией векторов нормалей перед использованием. Если вы не используете команду glScale, то нормализацию можно отключить без посторонних эффектов. По умолчанию эта опция выключена. [все]

Используйте связанные примитивы.

Связанные примитивы, такие как GL_LINES , GL_LINE_LOOP , GL_TRIANGLE_STRIP , GL_TRIANGLE_FAN , и GL_QUAD_STRIP уменьшают нагрузку на конвейер OpenGL, а также уменьшают количество данных, передаваемых графической подсистеме.

Растеризация

Растеризация часто является узким местом программных реализаций OpenGL.

Отключайте интерполяцию цветов, когда в этом нет необходимости

Интерполяция цветов включена по умолчанию. Плоское затенение не требует интерполяции четырех компонент цвета и, как правило, быстрее на программных реализациях OpenGL. Аппаратные реализации обычно выполняют оба вида затенения с одинаковой скоростью. Для отключения используйте команду glShadeModel(GL_FLAT) [П]

Отключайте тест на глубину, когда в этом нет необходимости.

Фоновые объекты, например, могут быть нарисованы без теста на глубину, если они визуализируется первыми [все]

Используйте отсечение обратных граней полигонов

Замкнутые объекты могут быть нарисованы с установленным режимом отсечения обратных граней glEnable(GL_CULL_FACE) Иногда это позволяет отбросить до половины многоугольников, не растеризуя их.[все]

Избегайте лишних операций с пикселями

Маскирование, альфа-смешивание и другие попиксельные операции могут занимать существенное время на этапе растеризации. Отключайте все операции, которые вы не используете. [все]

Уменьшайте размер окна или разрешение экрана

Простой способ уменьшить время растеризации – уменьшить число пикселей, которые будут нарисованы. Если меньшие размеры окна или меньшее разрешение экрана приемлемы, то это хороший путь для увеличения скорости растеризации. [все]

Текстурирование

Наложение текстур является дорогой операцией, как в программных, так и в аппаратных реализациях.

Используйте эффективные форматы хранения изображений

Формат GL_UNSIGNED_BYTE обычно наиболее всего подходит для передачи текстуры в OpenGL. [все]

Объединяйте текстуры в текстурные объекты или дисплейные списки.

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

Не используйте текстуры большого размера

Небольшие текстуры быстрее обрабатываются и занимают меньше памяти, что позволят хранить сразу несколько текстур в памяти графической подсистемы [all]

Комбинируйте небольшие текстуры в одну

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

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

Очистка буферов

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

Используйте команду glClear с осторожностью [все]

Очищайте все нужные буферы с помощью одной команды glClear.

Отключайте размывание (dithering)

Отключайте размывание перед очисткой буфера. Обычно различие между очистками с включенным размыванием и без него незаметно. [П]

Используйте ножницы (scissors) для очистки меньшей области

Если вы не хотите очищать весь буфер, используйте glScissor() для ограничения очистки по заданной области [все].

Не очищайте буфер цвета полностью

Если ваша сцена занимает только часть окна, нет необходимости очищать весь буфер цвета. [П]

Избегайте команды glClearDepth(d), где d!=1.0

Некоторые программные реализации оптимизированы для очистки буфера с глубиной 1.0. [П]

Разное

Проверяйте ошибки GL во время написания программ. [все]

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

Используйте glColorMaterial вместо glMaterial

Если в сцене материалы объектов различаются лишь одним параметром, команда glColorMaterial может быть быстрее, чем glMaterial [все]

Минимизируйте число изменений состояния OpenGL

Команды, изменяющие состояние OpenGL (glEnable/glDisable/glBindTexture и другие), вызывают повторные внутренние проверки целостности, создание дополнительных структур данных и т.д., что может приводить к задержкам [все]

Избегайте использования команды glPolygonMode

Если вам необходимо рисовать много незакрашенных многоугольников, используйте glBegin с GL_POINTS , GL_LINES , GL_LINE_LOOP или GL_LINE_STRIP вместо изменения режима рисования примитивов, так как это может быть намного быстрее [все]

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

Контрольные вопросы

Перечислите известные вам методы высокоуровневой оптимизации OpenGL-приложений

Почему предпочтительнее использование связанных примитивов?

Какая из двух команд выполняется OpenGL быстрее?

Структура GLUT-приложения

Далее будем рассматривать построение консольного приложения при помощи библиотеки GLUT. Эта библиотека обеспечивает единый интерфейс для работы с окнами вне зависимости от платформы, поэтому описываемая ниже структура приложения остается неизменной для операционных систем Windows, Linux и других.

Функции GLUT могут быть классифицированы на несколько групп по своему назначению:

Начало обработки событий

Регистрация функций с обратным вызовом

Управление индексированной палитрой цветов

Отображение дополнительных геометрических фигур (тор, конус и др.)

Инициализация проводится с помощью функции:

Переменная argcp есть указатель на стандартную переменную argc описываемую в функции main(), а argv – указатель на параметры, передаваемые программе при запуске, который описывается там же. Эта функция проводит необходимые начальные действия для построения окна приложения, и только несколько функций GLUT могут быть вызваны до нее. К ним относятся:

Первые две функции задают соответственно положение и размер окна, а последняя функция определяет различные режимы отображения информации, которые могут совместно задаваться с использованием операции побитового “или” ( “ | “ ) :

GLUT_RGBA Режим RGBA. Используется по умолчанию, если не указаны явно режимы GLUT_RGBA или GLUT_INDEX.
GLUT_RGB То же, что и GLUT_RGBA.
GLUT_INDEX Режим индексированных цветов (использование палитры). Отменяет GLUT_RGBA.
GLUT_SINGLE Окно с одиночным буфером. Используется по умолчанию.
GLUT_DOUBLE Окно с двойным буфером. Отменяет GLUT_SINGLE.
GLUT_STENCIL Окно с трафаретным буфером.
GLUT_ACCUM Окно с буфером накопления.
GLUT_DEPTH Окно с буфером глубины.

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

Работа с буфером маски и буфером накопления описана в главе 6.

Функции библиотеки GLUT реализуют так называемый событийно-управляемый механизм. Это означает, что есть некоторый внутренний цикл, который запускается после соответствующей инициализации и обрабатывает одно за другим все события, объявленные во время инициализации. К событиям относятся: щелчок мыши, закрытие окна, изменение свойств окна, передвижение курсора, нажатие клавиши, и «пустое» (idle) событие, когда ничего не происходит. Для проведения периодической проверки совершения того или иного события надо зарегистрировать функцию, которая будет его обрабатывать. Для этого используются функции вида:

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

Через glutReshapeFunc( ) устанавливается функция обработки изменения размеров окна пользователем, которой передаются новые размеры.

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

Функция, определяемая glutMotionFunс вызывается, когда пользователь двигает указатель мыши, удерживая нажатой кнопку мыши. glutPassiveMotionFunc регистрирует функцию, вызываемую, если пользователь двигает указатель мыши и не нажато ни одной кпопки мыши.

Контроль всех событий происходит внутри бесконечного цикла в функции

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

В случае , если приложение должно строить статичное изображение, можно заменить GLUT_DOUBLE на GLUT_SINGLE , так как одного буфера в этом случае будет достаточно, и убрать вызов функции glutIdleFunc().

Примитивы библиотек GLU и GLUT

Рассмотрим стандартные команды построения примитивов, которые реализованы в библиотеках GLU и GLUT.

Чтобы построить примитив из библиотеки GLU, надо сначала создать указатель на quadric-объект с помощью команды gluNewQuadric(), а затем вызвать одну из команд gluSphere(), gluCylinder(), gluDisk(), gluPartialDisk(). Рассмотрим эти команды отдельно:

Эта функция строит сферу с центром в начале координат и радиусом radius . При этом число разбиений сферы вокруг оси z задается параметром slices , а вдоль оси z – параметром stacks .

Данная функция строит цилиндр без оснований (то есть кольцо), продольная ось параллельна оси z, заднее основание имеет радиус baseRadius , и расположено в плоскости z=0, переднее основание имеет радиус topRadius и расположено в плоскости z= height . Если задать один из радиусов равным нулю, то будет построен конус.

Параметры slices и stacks имеют аналогичный смысл, что и в предыдущей команде.

Функция строит плоский диск (то есть круг) с центром в начале координат и радиусом outerRadius . При этом если значение innerRadius отлично от нуля, то в центре диска будет находиться отверстие радиусом innerRadius . Параметр slices задает число разбиений диска вокруг оси z, а параметр loops –число концентрических колец, перпендикулярных оси z.

Отличие этой команды от предыдущей заключается в том, что она строит сектор круга, начальный и конечный углы которого отсчитываются против часовой стрелки от положительного направления оси y и задаются параметрами startAngle и sweepAngle . Углы измеряются в градусах.

Команды, проводящие построение примитивов из библиотеки GLUT, реализованы через стандартные примитивы OpenGL и GLU. Для построения нужного примитива достаточно произвести вызов соответствующей команды.

Команда glutSolidSphere() строит сферу, а glutWireSphere()– каркас сферы радиусом radius . Остальные параметры те же, что и в предыдущих командах.

Команды строят куб или каркас куба с центром в начале координат и длиной ребра size .

Эти команды строят конус или его каркас высотой height и радиусом основания base , расположенный вдоль оси z. Основание находится в плоскости z=0.

Эти команды строят тор или его каркас в плоскости z=0. Внутренний и внешний радиусы задаются параметрами innerRadius , outerRadius . Параметр nsides задает число сторон в кольцах, составляющих ортогональное сечение тора, а rings – число радиальных разбиений тора.

Эти команды строят тетраэдр (правильную треугольную пирамиду) или его каркас, при этом радиус описанной сферы вокруг него равен 1.

Эти команды строят октаэдр или его каркас, радиус описанной вокруг него сферы равен 1.

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

Эти команды строят икосаэдр или его каркас, радиус описанной вокруг него сферы равен 1.

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

Настройка приложений OpenGL


Создание приложения в среде Borland C++ 5.02

Вначале необходимо обеспечить наличие файлов glut.h, glut32.lib, glut32.dll в каталогах BorlandC\Include\Gl, BorlandC\Lib, Windows\System соответственно. Также в этих каталогах надо проверить наличие файлов gl.h, glu.h, opengl32.lib, glu32.lib, opengl32.dll, glu32.dll, которые обычно входят в состав BorlandC++ и Windows. При этом надо учитывать, что версии Microsoft файлов opengl32.lib, glu32.lib, glut32.lib для Borland C++ не подходят, и следует использовать только совместимые версии. Чтобы создать такие версии, надо использовать стандартную программу ‘implib’, которая находится в каталоге BorlandC\Bin. Для этого надо выполнить команды вида

для перечисленных файлов, которые создают нужный *.lib файл из соответствующего *.dll файла. Кроме того, надо отметить, что компилятор BorlandC не может по неизвестным причинам использовать файл glaux.lib, входящий в состав BorlandC++5.02, при компиляции приложения, использующего библиотеку GLAUX, поэтому возможно от этой библиотеки придется отказаться. Для создания приложения надо выполнить следующие действия:

Создание проекта: для этого надо выбрать Project->New Project и заполнить поля в окне Target Expert следующим образом: в поле Platform выбрать Win32, в поле Taget Model выбрать Сonsole , нажать Advanced и отменить выбор пунктов ‘ *.rc ‘ и ‘ *.def ‘.

Подключить к проекту библиотеки OpenGL. Для этого надо выбрать в окне проекта название исполняемого файла проекта (*.exe) и, нажав правую кнопку мыши, выбрать в контекстном меню пункт Add node . Затем надо определить положение файлов opengl32.lib, glu32.lib, glut32.lib.

Для компиляции выбрать Project->Build All , для выполнения – Debug->Run .

Создание приложения в среде MS Visual C++ 6.0

Перед началом работы необходимо скопировать файлы glut.h, glut32.lib glut32.dll в каталоги MSVC\Include\Gl, MSVC\Lib, Windows\System соответственно. Также в этих каталогах надо проверить наличие файлов gl.h, glu.h, opengl32.lib, glu32.lib, opengl32.dll, glu32.dll, которые обычно входят в состав Visual C++ и Windows. При использовании команд из библиотеки GLAUX к перечисленным файлам надо добавить glaux.h, glaux.lib.

Для создания приложения надо выполнить следующие действия:

Создание проекта: для этого надо выбрать File->New->Projects->Win32 Console Application , набрать имя проекта, OK.

В появившемся окне выбрать ‘ An empty project ’, Finish,OK.

Текст программы можно либо разместить в созданном текстовом файле (выбрав File->New->Files->Text File ), либо добавиьб файл с расширением *.c или *.cpp в проект (выбрав Project->Add To Project->Files ).

Подключить к проекту библиотеки OpenGL. Для этого надо выбрать Project->Settings->Link и в поле Object/library modules набрать названия нужных библиотек: opengl32.lib, glu32.lib, glut32.lib и, если надо, glaux.lib.

Для компиляции выбрать Build->Build program.exe , для выполнения – Build->Execute program.exe .

Чтобы при запуске не появлялось текстовое окно, надо выбрать Project->Settings->Link и в поле Project Options вместо ‘subsystem:console’ набрать ‘subsystem:windows’, и набрать там же строку ‘/entry:mainCRTStartup’

Когда программа готова, рекомендуется перекомпилировать ее в режиме ‘Release’ для оптимизации по быстродействию и объему. Для этого сначала надо выбрать Build->Set Active Configuration… и отметить ‘…-Win32 Release’, а затем заново подключить необходимые библиотеки.

Демонстрационные программы


Пример 1: Простое GLUT-приложение



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

Пример 2: Модель освещения OpenGL

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

Пример 3: Текстурирование

Результатом выполнения этой программы является построение тетраэдра с вращающимися вокруг него кольцами, на которые нанесена текстура. В среде MS Visual C++ программа может компилироваться без изменений, а при компиляции в Borland C++ придется закомментировать вызов и описание функции TextureInit(), после чего не будет проводиться наложение текстур. Как было сказано выше, попытка использовать функции из библиотеки GLAUX приводит к сообщению об ошибке при компиляции программы.

При компиляции программы в MS Visual C++ файл ‘texture.bmp’ надо поместить в каталог проекта или указать полный путь к нему, используя символ ‘/’. Если путь не указан, то при запуске исполняемого файла из операционной системы, файл с текстурой должен находиться в том же каталоге.

Примеры практических заданий


Cornell Box

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

Требуется создать изображение сцены Cornell Box. Эта классическая сцена представляет собой комнату кубического вида, с отсутствующей передней стенкой. В комнате находятся геометрические предметы различных форм и свойств (кубы, параллелепипеды, шары), и протяженный источник света на потолке. Присутствует также камера с заданными параметрами (обычно она расположена так, чтобы была видна вся комната). В одной из лабораторий Корнельского университета ( http://graphics.cornell.edu ) такая комната существует в реальности, и ее фотографии сравниваются с изображениями, построенными методами трассировки лучей для оценки точности методов. На странице лаборатории можно найти описание геометрии сцены в текстовом формате.

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

Протяженный источник света на потолке комнаты можно эмулировать несколькими точечными источниками.

За простейшую реализацию сцены ставится 7 баллов.

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

Наложение текстур на объекты сцены поощряется 2-мя баллами.

Дополнительными баллами оценивается присутствие в сцене теней. Один из простейших алгоритмов наложения теней приведен в разделе 0. За его реализацию можно получить до 2 баллов. Использование более продвинутых алгоритмов (например, shadow volumes) будет оценено дополнительными баллами.

Реализация устранения ступенчатости (antialiasing) методом, предложенным в разделе 0. или каким-либо другим оценивается в 2 балла.

За введение в сцену прозрачных объектов и корректный их вывод дается 1 балл. Механизм работы с прозрачными объектами описан в разделе 0.

Задание оценивается, исходя из 15 баллов.

В приведенной ниже таблице указано распределение баллов в зависимости от реализованных требований:

Простейший вариант сцены (только освещение) 7 баллов
Разбиение полигонов +1 балл
Использование текстур +2 балла
Наложение теней +2 балла
Устранение ступенчатости +2 балла
Использование прозрачных объектов +1 балл

Дополнительные баллы можно получить за хорошую оптимизацию программы, необычные решения, эстетичность и т.д.

Виртуальные часы

Целью задания является создание трехмерной интерактивной модели аналоговых часов.

Обязательные требования к программе:

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

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

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

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

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

Пожелания к программе:

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

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

Приветствуется выполнение задания в виде демонстрации, т.е. c возможностью работы в полноэкранном режиме и немедленным выходом по клавише Escape. Можно написать программу как Screen Saver.

Постарайтесь использовать максимум возможностей OpenGL. Блики, отражения, спецэффекты — за все это обязательно даются дополнительные баллы.

Проявите вкус — сделайте так, чтобы нравилось прежде всего Вам. Но не увлекайтесь — оставайтесь реалистами.

Максимальная оценка — 20 баллов. За минимальную реализацию требований ставиться 10 баллов. Еще до 10 баллов можно получить за использование в работе возможностей OpenGL (текстур, прозрачности , environment mapping и пр.), оригинальных и продвинутых алгоритмов, количество настроек, а также за эстетичность и красоту сцены.

Интерактивный ландшафт

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

Обязательная часть задания

Для выполнения обязательной части задания необходимы:

генерация трехмерного ландшафта

раскраска для придания реалистичности

возможность «полета» над ландшафтом (управление)

Более подробное описание:

Один из вариантов задания поверхности ландшафта — задание так называемого «поля высот» — функции вида z=f(x, y), которая сопоставляет каждой точке (x, y) плоскости OXY число z — высоту поверхности ландшафта в этой точке. Один из способов задания функции f — табличный, когда функция f представляется матрицей T размера M x N, и для целых x и y f=T[x, y], а для дробных x и y из диапазонов [0..M-1] и [0..N-1] соответственно f вычисляется интерполяцией значений f в ближайших точках плоскости OXY с целыми x и y, а вне указанных диапазонов x и y значение функции считается неопределенным.

Допустим, в памяти лежит двухмерный массив со значениями матрицы T. Пусть N=M. Если теперь для каждого квадрата [x, x+1] x [y, y+1], где x и y принадлежат диапазону [0..N-2] построить две грани: ((x, y, T[x, y]), (x+1, y, T[x+1, y]), (x+1, y+1, T[x+1, y+1])) и ((x, y, T[x, y]), (x+1, y+1, T[x+1, y+1]), (x, y+1, T[x, y+1])), то мы получим трехмерную модель поверхности, описываемой матрицей Т.

Но каким образом задать массив значений матрицы Т? Один из способов — сгенерировать псевдослучайную поверхность с помощью фрактального разбиения. Для этого положим размерность матрицы T равной 2^N+1, где N — натуральное число. Зададим некоторые произвольные (псевдослучайные) значения для четырех угловых элементов матрицы Т. Теперь для каждого из четырех ребер матрицы Т (это столбцы или строки элементов, соединяющие угловые элементы) вычислим значение элемента матрицы Т, соответствующего середине ребра. Для этого возьмем среднее арифметическое значений элементов матрицы Т в вершинах ребра и прибавим к получившемуся значению некоторое псевдослучайное число, пропорциональное длине ребра. Значение центрального элемента матрицы Т вычислим аналогично, только будем брать среднее арифметическое четырех значений элементов матрицы в серединах ее ребер.

Теперь разобьем матрицу Т на четыре квадратные подматрицы. Значения их угловых элементов уже определены и мы можем рекурсивно применить к подматрицам Т описанную выше процедуру. Будем спускаться рекурсивно по дереву подматриц, пока все элементы Т не будут определены. С помощью подбора коэффициентов генерации псевдослучайной добавки можно регулировать «изрезанность» поверхности. Для реалистичности поверхности важно сделать величину псевдослучайной добавки зависящей от длины текущего ребра — с уменьшением размера ребра должно уменьшаться и возможное отклонение высоты его середины от среднего арифметического высот его вершин.

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

Внимание: использование NURBS возможно, но не приветствуется в силу ограниченности использования NURBS для реальных приложений.

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

Для еще большего реализма и для подчеркивания рельефа осветить модель ландшафта бесконечно удаленным источником света (как бы солнцем).

Цвет вершин можно задавать через glColor*() совместно с glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);

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

Элементарное управление движением камеры по клавиатурным «стрелочкам». Нажатие на стрелку «вверх» — передвижение по направлению взгляда вперед. «Назад» — по направлению взгляда назад. «Влево», «Вправо» по аналогии, «Page Up», «Page Down» — вверх, вниз, соответственно.

В GLUT’е получать нажатия не алфавитно-цифровых клавиш можно через функцию glutSpecialFunc(void (*)(int key, int x, int y)), где key — константа, обозначающая клавишу (см. в glut.h — GLUT_KEY_ ). Функция используется аналогично glutKeyboardFunc().

Движение мыши в горизонтальной плоскости (смещение по оси X) управляет углом поворота направления взгляда в горизонтальной плоскости (альфа, от 0 до 2*PI). Движение мыши в вертикальной плоскости (смещение по оси Y) управляет углом поворота направления взгляда в вертикальной плоскости относительно горизонта (бета, от -PI до PI). Зная оба угла, вектор направления взгляда в мировых координатах вычисляется следующим образом:

direction_x = cos(альфа) * cos(бета);

direction_y = sin(альфа) * cos(бета),

а затем нормализуется.

Вектор направления «вбок» вычисляется как векторное произведение вектора направления вертикально вверх, то есть вектора (0, 0, 1) и уже известного вектора направления взгляда.

Вектор направления «вверх» вычисляется как векторное произведение вектора направления взгляда и вектора направления «вбок».

Положение камеры в OpenGL можно передать через gluLookAt(). Подсказка: параметр target можно положить равным position + direction

Смещение позиции камеры должно происходить не на фиксированное расстояние за один кадр, а вычисляться, исходя из скорости передвижения камеры, и времени, ушедшего на обсчет последнего кадра. Передвижение камеры должно осуществляться в направлении взгляда. Скажем, по левой кнопке мыши — вперед, а по правой — назад. Для того, чтобы засечь время, можно воспользоваться функцией timeGetTime(), описанной в «mmsystem.h», и реализованной в библиотеке «winmm.lib» (только для Windows)

В GLUT’е для этого есть специальный вызов

time = glutGet(GLUT_ELAPSED_TIME) (аналогично timeGetTime())

Вода, или нечто на нее похожее

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

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

Отражения в воде

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

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

База
Ландшафт 8 баллов
Раскраска 2 балла
Управление 2 балла
Дополнительно
Управление мышью +2 балла
Объекты +3 балла
Вода +4 балла
Отражение +4 балла
*Тени +5 баллов
Всего 30 баллов

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

Дополнительные источники информации:

Литература

Эйнджел Э. Интерактивная компьютерная графика. Вводный курс на базе OpenGL, 2 изд. Пер. с англ.- Москва, «Вильямс», 2001.

Порев В.Н. Компьютерная графика. СПб., BHV, 2002.

Шикин А. В., Боресков А. В. Компьютерная графика. Полигональные модели. Москва, ДИАЛОГ-МИФИ, 2001.

Тихомиров Ю. Программирование трехмерной графики. СПб, BHV, 1998.

Performance OpenGL: Platform Independent Techniques. SIGGRAPH 2001 course.

OpenGL performance optimization, Siggraph’97 course.

Visual Introduction in OpenGL, SIGGRAPH’98.

The OpenGL graphics system: a specification (version 1.1).

Программирование GLUT: окна и анимация. Miguel Angel Sepulveda, LinuxFocus.

The OpenGL Utility Toolkit (GLUT) Programming Interface, API version 3, specification.

Программа — шаблон для изучения OpenGL

Здравствуйте.
Хочу изучать програмирование графики с использованием OpenGL, но я еще не изучал програмирование под ОСи, поэтому создавать окна для рисования не умею.
В книге нашел такую вот заготовку :
Файл basis.h

Compiling.
Zagotovka.cpp
Linking.
Zagotovka.obj : error LNK2001: unresolved external symbol _auxMainLoop@4
Zagotovka.obj : error LNK2001: unresolved external symbol _auxReshapeFunc@4
Zagotovka.obj : error LNK2001: unresolved external symbol _auxQuit@0
Zagotovka.obj : error LNK2001: unresolved external symbol _auxInitWindowA@4
Zagotovka.obj : error LNK2001: unresolved external symbol _auxInitDisplayMode@4
Zagotovka.obj : error LNK2001: unresolved external symbol _auxInitPosition@16
Debug/Zagotovka.exe : fatal error LNK1120: 6 unresolved externals
Error executing link.exe.

Zagotovka.exe — 7 error(s), 0 warning(s)

Использую VC++ 6.0.

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

добавь ещё вот такие директивы

Спасибо большое!Все заработало.

Возникла новая проблема, теперь с функцией draw.
Прошу знающих людей добавить в эту функцию инициализирующие действия, то есть определить формат пикселов, смоздать и в конце ф-ии удалить контекст воспроизведения.Если не сложно, напишите пожалуйста как все это сделать, желательно с комментариями.
Очень хочеться начать изучение OGL, даже на спецкурс в универе записался и курсовую взял на эту тему, но из за недостатка знаний в других областях програмирования(все таки 2-ой курс еще) никак не получается(например, проблема с окном).
Заранее благодарен.

для начала — выкинуть и забыть glaux, и перейти на glut (для скромных студенческих программок). а потом — читать, читать, и еще раз читать хелп к нему.

Я бы с радостью хелп почитал, да с инглишем туго :(, надо на курсы срочно бежать.
glaux — используется потому что, как я уже писал, пример взят с книжки.
Почитал статейку, которую указал Гусли, появились новые вопросы.Сначала надо установить стили окна WS_CLIPSIBLINGS и WS_CLIPCHILDREN.Так как окно уже создано, надо воспользоваться функцией SetWindowLong().Как ее использовать?Как я понял из всплывающей подсказки, первый параметр это ссылка на окно(по крайне мере в делфи это вроде так называется).
Я объявил переменную типа HWND, как теперь получить эту ссылку?
Пока вроде все.

N0rd
Я знаю эту книжку. Мой совет: ВЫКИНЬ ЕЕ НА ФИГ, А ЛУЧШЕ СОЖГИ. Это самая худшая книга для начального изучения, я из-за нее 2 года не мог начать изучать OpenGL.
Начни с книги И. Тарасова и уроков NeHe(есть на русском).

N0rd
>Почитал статейку, которую указал Гусли, появились новые вопросы.Сначала надо установить стили окна WS_CLIPSIBLINGS и WS_CLIPCHILDREN.Так как окно уже создано, надо воспользоваться функцией SetWindowLong().Как ее использовать?

Твой пример написан для консольного проекта. И в нем инициализацию окна на себя берут функции библиотеки glaux. То, что ты спрашиваешь относится уже к проекту Win32 API. В котором окно надо создавать самому и настаривать тоже самому.

Вообщем забросил я идею создавать окно и использованием той книги и glaux.Нашел библиотеку glut, в другой книге нашел пример програмки рисующей окно.

Все разобрался, рисовать надо в функции display.Вопрос закрыт :).

22 материала для изучения OpenGL

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

Параметр face команды glMaterialfv

Свойства задаются для лицевой стороны поверхности .

Свойства задаются для тыльной стороны поверхности .
Имя параметра Значение по умолчанию
цвет фонового отражения материала
цвет рассеянного отражения материала
цвет зеркального отражения материала
цвет собственного излучения материала
степень в формуле зеркального отражения (коэффициент блеска). Допускаются значения в интервале [0; 128].
Свойства задаются для тыльной стороны поверхности .
Управление фоновым отражением.
Управление рассеянным отражением.
Управление зеркальным отражением.

Позднее я добавил еще две папки:

include для хранения заголовочных файлов сторонних библиотек
lib для хранения двоичных файлов сторонних библиотек
src для хранения моего собственного кода (как заголовочных файлов, так и файлов исходного кода)
build для хранения двоичного файла моей программы

Чтобы изменить выходную папку, в которую помещается построенный исполняемый файл программы (по-умолчанию он помещается в папку $(SolutionDir)\$(Configuration)), я должен был поменять некоторые свойства проекта (через меню Project->Properties в Visual Studio):

Project -> Properties -> General -> Output Directory
$(SolutionDir)\build\$(Configuration)\

Project -> Properties -> Debugging -> Working Directory
$(OutDir)

$(SolutionDir), $(Configuration), $(OutDir) и прочее — это так называемые макросы для команд и свойств построения. При построении проекта они подменяются своими значениям. Например, $(Configuration) заменяется на Debug или Release.

Также надо было добавить папку include в пути поиска заголовочных файлов

Пространства имен

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

shaders для хранения шейдеров
textures для хранения текстур
opengl Несмотря на то, что API OpenGL основан на языке C, он, тем не менее — объектно-ориентированный в том же смысле, в каком объектно-ориентированным является и WinAPI. Сплошь и рядом функции OpenGL создают какие-то объекты и возвращают их так называемые дескрипторы (или хэндлы) — числовые идентификаторы, при помощи которых программа может на эти объекты ссылаться. В общем, API OpenGL легко и естественно можно перевести в объектно-ориентированный сиплюсплюсный вид, что я и сделал, создав такие классы как Shader, Buffer, FramebufferObject, Renderbuffer, ProgramPipeline и другие и поместив их в пространство имен opengl.
win В это пространство имен я поместил все классы и функции, которые используют WinAPI и поэтому являются специфичными для ОС Windows. Например, классы Window, HighResolutionTimer, winapi_error и другие.
engine В это пространство имен я помещал классы, которые воспринимаются мной как «высокий уровень абстракции». Например классы Camera, Mesh, Model3D, Skybox, ShadowMap и другие. Эти классы зависят от классов в пространстве имен opengl.
util В это пространство имен я помещал классы и функции, которые выполняли роль полезных в хозяйстве инструментов. Типичный пример — функции из пространства имен util::Requires, которые например проверяют корректность аргументов, переданных в функцию и другие условия. Также в пространство имен util попали функции file::ReadAllText, file::getFileExtension, класс Event и пр.
vmath Сюда попали классы Vector и Matrix и всевозможные функции для работы с матрицами и векторами. Эти классы зависят от платформы Intel, поскольку используют Streaming SIMD Extensions (SSE), впрочем использование SSE можно отключить при помощи условной компиляции.

Замечу, что среди приведенных пространств имен только пространство win содержит платформозависимый код. Во всяком случае, я надеюсь, что это так. Приведенные выше пространства имен отражают модульность программы. Эти пространства не зависят друг от друга (за исключением engine, которое зависит от всех остальных пространств имен) и поэтому их можно разрабатывать независимо, а это — большой плюс для проекта.

Загрузка функций OpenGL

Итак, я хочу использовать в своей программе некий API на языке C. Обычно это значит вот что. API представляет собой набор функций. Прототипы (сигнатуры) этих функций находятся в некоем заголовочном файле (или файлах), а сами функции находятся в двоичном файле библиотеки (в ОС Windows такие файлы имеют расширение .lib или .dll). Также существует документация, в которой написано, что каждая функция делает и как вообще эти функции следует использовать.

В OpenGL это не совсем так. В каждой ОС существует библиотека, которая содержит ограниченный набор OpenGL’евских функций. В Windows это библиотека opengl32.dll, которая содержит набор функций, соответствующих устаревшей сто лет назад версии OpenGL 1.1. С тех пор огромное количество новых функций было добавлено в API, к тому же многие функции теперь объявлены устаревшими (т. е. ими буквально нельзя пользоваться). Предполагается загружать новые функции динамически, т. е. получать адреса функций во время выполнения программы. Для этого в OpenGL всегда был предусмотрен так называемый OpenGL extension mechanism — механизм, позволяющий производителям видеокарт добавлять в API новые функции, а пользователям (программистам) — эти функции вызывать. Но если эти функции не находятся в opengl32.dll, то где они тогда лежат? Ответ: в библиотеке, которая входит в состав ПО, которое поставляется вместе с вашей видеокартой. Например для видеокарт NVIDIA файл OpenGL’евской библиотеки может называться как-то так: nvogl32.dll. Однако программист, разумеется, не должен компоновать свою программу с библиотекой производителя видеокарты (в противном случае его программа была бы плохо переносимой), вместо этого программа запрашивает указатели на функции API во время выполнения. Каким образом это делается, подробно описано в статье Load OpenGL Functions. Скажу только, что в Windows для получения указателя на какую либо функцию OpenGL используется функция под названием wglGetProcAddress (заголовочный файл wingdi.h, библиотека opengl32.dll; все функции, начинающиеся с wgl, относятся к ОС Windows). Функция wglGetProcAddress принимает в качестве параметра имя искомой функции. Что происходит у wglGetProcAddress «под капотом» для меня — тайна, покрытая мраком. Загружает ли она в память приложения библиотеку от производителя видеокарты и если да, то как она ее находит? Почему для ее вызова необходимо создать контекст OpenGL (об этом ниже)? Впрочем это не столь важно, так как на самом деле рекомендуется перепоручать загрузку всех функций OpenGL специальным библиотекам, которые называются OpenGL Loading Libraries. Одной из таких библиотек является OpenGL Extension Wrangler Library (GLEW), которую я и использовал в своем проекте.

Библиотека OpenGL Extension Wrangler Library (GLEW)

Библиотека GLEW предназначена для загрузки функций API OpenGL. Что это значит? Предположим, вам надо вызвать какую-нибудь OpenGL’евскую функцию, например glAttachShader. Чтобы вызвать функцию, вам нужен ее адрес. Адрес функции можно получить при помощи wglGetProcAddress. Поскольку вы собираетесь вызывать функцию glAttachShader много раз в разных местах программы, ее адрес надо сохранить в некой глобальной переменной. Эта переменная должна иметь тип указателя на функцию, сигнатура которой совпадает с сигнатурой функции glAttachShader, описанной в спецификации OpenGL. И таких функций как glAttachShader — сотни. Для каждой из них в библиотеке GLEW имеется глобальная переменная, которая хранит адрес функции. А значения этим переменным присваиваются функцией glewInit. Ниже показан фрагмент заголовочного файла glew.h, в котором объявлены прототип функции glAttachShader и указатель на нее. Я снабдил этот фрагмент своими комментариями для большей понимабельности.

// The most important function in GLEW library which fills a bunch of global function pointers
// with the adresses of OpenGL functions.
GLEWAPI GLenum GLEWAPIENTRY glewInit ( void ) ;

// No idea what this is used for 🙂
#ifndef GLEW_GET_FUN
#define GLEW_GET_FUN(x) x
#endif

// Pointer-to-function type definition.
typedef void ( GLAPIENTRY * PFNGLATTACHSHADERPROC ) ( GLuint program, GLuint shader ) ;

// The pointer to function glAttachShader.
GLEW_FUN_EXPORT PFNGLATTACHSHADERPROC __glewAttachShader ;

// Macrodefinition which translates all references to glAttachShader to __glewAttachShader.
#define glAttachShader GLEW_GET_FUN(__glewAttachShader)

Итак, чтобы воспользоваться библиотекой GLEW, надо

    Скачать библиотеку GLEW с оф. сайта. Из скачанного архива мне нужны только четыре файла: glew.h, wglew.h (этот заголовочный файл нужен, если программа предназначается для ОС Windows), glew32.lib и glew32.dll, которые я распихиваю в папки своего проекта. glew.h, wglew.h — в папку \include\GL , glew32.lib — в папку \lib\GLEW , glew32.dll — в папки \build\Debug и \build\Release . В своем проекте я использую динамическую компоновку с библиотекой glew32.dll, однако можно использовать и статическую — тогда файл glew32.dll не нужен, а вместо файла glew32.lib надо взять файл glew32s.lib. Включение заголовочных файлов GLEW и подключение библиотек glew32.lib и opengl32.lib я поместил в файл GLEWHeaders.h:

// GLEWHeaders.h
#pragma once

// Link to opengl32.lib
#pragma comment(lib, «opengl32.lib»)

// Link to glew32.lib
#pragma comment(lib, «glew32.lib»)

// You can also statically link GLEW library as follows:
// #pragma comment(lib, «glew32s.lib»)
// #define GLEW_STATIC

#include «GL/glew.h»
#include «GL/wglew.h»

С вызовом glewInit связаны некоторые трудности. Оказывается, для того, чтобы вызвать glewInit, надо сначала создать так называемый OpenGL rendering context (далее — rendering context или контекст рисования). Функция wglGetProcAddress тоже не будет работать без созданного rendering context’а. Что же это такое — контекст рисования? Попробую дать определение.

  • Rendering context представляет собой некий объект, обладающий состоянием. Это состояние влияет на результат (или управляет результатом) работы функций OpenGL. В частности, состояние, в котором находится rendering context, определяет изображение, которое формируется на экране компьютера. Вызовы функций OpenGL могут изменять состояние rendering context’а. Пользовательская программа может взаимодействовать с rendering context’ом только при помощи функций OpenGL.
  • Могу предположить, что rendering context хранится в памяти драйвера видеокарты.
  • Пользовательская программа может создавать (функции wglCreateContext и wglCreateContextAttribsARB) и уничтожать (функция wglDeleteContext) rendering context. При создании контекста программа указывает, каким требованиям он должен удовлетворять (например, поддерживать определенную версию OpenGL).
  • Rendering context как правило существует в приложении в единственном экземпляре. Однако можно создать в одной программе несколько rendering context’ов. Если в программе есть несколько rendering context’ов, то в конкретный момент времени только один из них может быть активным (или текущим). Активный rendering context — это тот, который в данный момент влияет на результат работы функций OpenGL (т. е. например на отрисовку изображения на экране) и на состояние которого влияют вызовы функций OpenGL. Программа может переключаться между несколькими rendering context’ами (делать текущим то один, то другой контекст — см. функцию wglMakeCurrent). Понятие «активный контекст» принадлежит потоку управления, т. е. каждый поток управления может иметь свой активный контекст OpenGL. Заметим, что нежелательно, чтобы два разных потока управления имели одинаковый активный контекст OpenGL.
  • В Windows rendering context связан с так называемым контекстом устройства (device context — даже не хочу разбираться, что это такое, можете почитать статью Device Contexts) или проще говоря — с окном. Без окна нельзя создать контекст рисования. Соответственно, в этом окне и будет отрисовываться изображение, формируемое программой путем вызова различных функции OpenGL. При этом во время выполнения программы можно связывать любой контекст рисования с любым контекстом устройства. Можно даже связать один контекст рисования с несколькими контекстами устройств (но не наоборот) — см. функцию wglMakeCurrent. Тогда вы получите одно и то же изображение, отрисованное в нескольких окнах.
  • Без контекста рисования нельзя ничего нарисовать на экране компьютера.

Почему надо непременно создать контекст рисования, чтобы вызвать glewInit? Возможно это связано с тем, что в Windows значения указателей на функции, которые возвращает wglGetProcAddress, зависят от текущего контекста рисования… О’кей, давайте создадим этот контекст, тем более, что для рисования все равно нужен контекст рисования. Но оказывается, что контекст контексту рознь. По-настоящему работающий контекст рисования (при помощи которого можно что-то нарисовать) можно создать только при помощи функций wglChoosePixelFormatARB и wglCreateContextAttribsARB. Но адресов этих функций у нас нет, а чтобы их получить, нам надо либо вызвать glewInit либо вызвать wglGetProcAddress, но ни то, ни другое нельзя сделать без контекста рисования. Замкнутый круг? К счастью существует старинная функция для создания контекста рисования wglCreateContext, которая экспортируется непосредственно библиотекой opengl32.dll и поэтому ее адрес у нас есть изначально. Хотя такой контекст рисования будет поддерживать только версию OpenGL 1.1, и рисовать что-либо с его помощью нецелесообразно, он позволит нам вызвать glewInit. Затем мы его удалим и создадим новый нормальный контекст. Всю работу по инициализации GLEW я поместил в функцию Initialize_GLEW_Library в файлах GlewInitializer.h и GlewInitializer.cpp (см. листинг ниже).
Итак, алгоритм инициализации библиотеки GLEW следующий:

  • Создать окно. Окно нужно, так как без него нельзя создать контекст рисования. Кстати, созданное окно мы не отображаем на экране.
  • Создать контекст рисования при помощи функции wglCreateContext. Сделать его активным при помощи функции wglMakeCurrent.
  • Вызвать функцию glewInit.
  • Уничтожить окно. Для этого надо послать ему сообщение WM_DESTROY при помощи функции DestroyWindow. Это сообщение надо еще и обработать в цикле обработки сообщений, именно поэтому, несмотря на то, что окно мы нигде не отображаем, цикл обработки сообщений запускать все же приходится.
  • Уничтожить созданный ранее контекст рисования (поскольку он больше не нужен) при помощи функции wglDeleteContext.

// GlewInitializer.h
#pragma once

namespace opengl
<
// Initializes GLEW library
void Initialize_GLEW_Library ( ) ;
>

#include
#include «GlewInitializer.h»
#include «winapi_error.h»
#include «GlException.h»

namespace opengl
<
LRESULT CALLBACK FalseWndProc (
HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam )
<
switch ( message )
<
case WM_DESTROY :
<
PostQuitMessage ( 0 ) ;
break ;
>
default :
<
return DefWindowProc ( hWnd, message, wParam, lParam ) ;
>
>

HGLRC CreateFalseRenderingContext ( HDC hDC )
<
PIXELFORMATDESCRIPTOR pfd ;

// Choose a stub pixel format in order to get access to wgl functions.
:: SetPixelFormat (
hDC, // Device context.
1 , // Index that identifies the pixel format to set. The various pixel formats supported by a device context are identified by one-based indexes.
& pfd ) ; // [out] Pointer to a PIXELFORMATDESCRIPTOR structure that contains the logical pixel format specification.

// Create a fiction OpenGL rendering context.
HGLRC hGLRC = wglCreateContext ( hDC ) ;

// Throw exception if failed to create OpenGL rendering context.
if ( hGLRC == NULL )
throw win :: make_winapi_error ( «opengl::CreateFalseRenderingContext() -> wglCreateContext()» ) ;

// Make just created OpenGL rendering context current.
if ( ! wglMakeCurrent ( hDC, hGLRC ) )
throw win :: make_winapi_error ( «opengl::CreateFalseRenderingContext() -> wglMakeCurrent()» ) ;

void Initialize_GLEW_Library ( )
<
static bool s_IsInitialized = false ;

if ( s_IsInitialized )
return ;

const char * wnd >= «FalseWindow» ;
HINSTANCE hInst = GetModuleHandle ( NULL ) ; // get application handle

// Create a struct describing window class.
WND >;
wcex. cbSize = sizeof ( WND >) ; // struct size
wcex. style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC ; // window style
wcex. lpfnWndProc = FalseWndProc ; // pointer to window function WndProc
wcex. cbClsExtra = 0 ; // shared memory
wcex. cbWndExtra = 0 ; // number of additional bytes
wcex. hInstance = hInst ; // current application’s handle
wcex. hIcon = LoadIcon ( hInst, MAKEINTRESOURCE ( >) ) ; // icon handle
wcex. hCursor = LoadCursor ( NULL , >) ; // cursor handle
wcex. hbrBackground = ( HBRUSH ) ( COLOR_MENU + 1 ) ; // background brush’s handle
wcex. lpszMenuName = NULL ; // pointer to a string — menu name
wcex. lpszClassName = wnd >; // pointer to a string — window class name
wcex. hIconSm = LoadIcon ( hInst, MAKEINTRESOURCE ( >) ) ; // small icon’s handle

// Create a FICTION window based on the previously registered window class.
HWND hWnd = CreateWindow ( wnd >// window class name
wnd >// window title
WS_OVERLAPPEDWINDOW, // window type
CW_USEDEFAULT, CW_USEDEFAULT, // window’s start position (x, y)
100 , // window’s width in pixels
100 , // window’s height in pixels
NULL , // parent window
NULL , // menu handle
hInst, // application handle
NULL ) ; // pointer to an object passed to the window with CREATESTRUCT struct (field lpCreateParams), pointer to which is contained in lParam parameter of WM_CREATE message

// Throw exception if CreateWindow() failed.
if ( ! hWnd )
throw win :: make_winapi_error ( «opengl::Initialize_GLEW_Library() -> CreateWindow()» ) ;

// Get device context for the window.
HDC hDC = GetDC ( hWnd ) ;

// Create a fiction rendering context.
HGLRC tempOpenGLContext = CreateFalseRenderingContext ( hDC ) ;

// Initialize GLEW (is possible only if an OpenGL rendering context is created).
if ( glewInit ( ) ! = GLEW_OK )
throw GlException ( «opengl::Initialize_GLEW_Library() -> glewInit()» ) ;

wglMakeCurrent ( NULL , NULL ) ; // remove the temporary context from being active
wglDeleteContext ( tempOpenGLContext ) ; // delete the temporary OpenGL context
>
>

В приведенном коде используются классы win::winapi_error и opengl::GlException — это всего-навсего классы исключений для ошибок, связанных соответственно с WinAPI и OpenGL, приводить их код здесь не буду.

На этом все. В следующей заметке я расскажу о том, как создать окно (рисовать-то мы будем именно в окне).

Мастер Йода рекомендует:  Инструкция получаем число подписчиков в Twitter, не используя API
Добавить комментарий