Анимация
Анимация
На примере многочисленных хранителей экрана (screen-saver) вы видели, как гладко работает анимация в OpenGL. OpenGL использует два буфера памяти (front and back). Первый (front-буфер) отображается на экране, второй в это время может обрабатываться процессором. Когда обработка закончится, то есть очередная сцена будет готова, вы можете произвести быстрое переключение буферов (swap), обеспечивая тем самым гладкую анимацию изображения. При обмене копирование массивов не происходит, изменяется лишь значение указателя (адреса) отображаемого блока памяти. Отметьте, что процесс рисования в back-буфер происходит быстрее, чем в front, так как большинство видеокарт запрещают редактировать изображение в момент вертикальной развертки, а это происходит 60-90 раз в секунду.
Рассмотрим основную схему алгоритма анимации, используемого в OpenGL-при-ложениях. В кино эффект движения достигается тем, что каждый кадр проецируется на экран в течение короткого промежутка времени, затем шторка проектора моментально закрывается, пленка продвигается на один кадр, вновь открывается шторка и цикл повторяется. Период цикла равен 1/24 с или даже 1/48 с в современных кинопроекторах. Современные компьютеры допускают смену кадра (refresh rate) до 120 раз в секунду. Рассмотренный алгоритм можно записать так в цикле по всем кадрам:
сотри старое изображение;создай новое изображение;
задержи изображение на какое-то время.
Если реализовать анимацию по такой схеме, то эффект будет тем более удручающим, чем ближе к 1/24 с подходит время создания изображения, так как полное изображение существует на экране лишь долю периода. Большую часть периода мы видим процесс рисования.
OpenGL предоставляет возможность двойной буферизации (аппаратной или программной — зависит от видеокарты). Алгоритм анимации в этом случае таков: пока проецируется первый кадр, создается второй. Переключение кадров происходит только после того, как закончится формирование второго кадра. Пользователь никогда не видит незавершенный кадр. Эту схему можно представить в виде проектора с двумя кадрами пленки. В момент демонстрации первого второй стирается и вновь рисуется. Новый алгоритм можно записать в цикле по кадрам:
Создай в нем новое изображение.
Переключи буферы (front-back).
Последний шаг алгоритма не начнет выполняться, пока не закончится предыдущий шаг — создание нового кадра в back-буфере. Ожидание этого события (конец рисования в невидимый буфер) дополняется ожиданием завершения цикла прямого хода развертки экрана. Поэтому самая большая частота смены изображений равна текущему значению частоты кадров дисплея. Допустим, что эта частота равна 60 Гц, тогда частота смены изображений будет 60 fps (frames per second — кадров в секунду). Если время рисования занимает немногим более 1/60 с (пусть 1/45 с), то один и тот же кадр будет проецироваться два такта цикла развертки и частота смены изображений реально будет 30 fps. Промежуток времени между 1/30 с и 1/45 с процессор простаивает (is idle). Если время подготовки невидимого кадра нестабильно (плавает), то частота смены изображений может измениться скачком, что воспринимается как неприятная помеха. Для сглаживания этого эффекта иногда искусственно добавляют небольшую задержку, с тем чтобы немного снизить частоту, но сделать ее стабильной. Отметьте, что OpenGL не имеет команды переключения буферов, так как такая команда всегда зависит от платформы. Мы будем пользоваться функцией SwapBuf f ers(HDC hdc), входящей в состав Windows API.
Другие функции OpenGL
Другие функции OpenGL
Другие функции OpenGL позволяют размещать объекты на трехмерной сцене, выбирать точку размещения глаза наблюдателя (камеру), передвигать эту точку. Неотъемлемой частью трехмерной графики является освещение материалов. Конвейер OpenGL использует специальные алгоритмы подсчета цвета любого фрагмента с учетом заданных свойств материала и источников света. Моделирование атмосферных эффектов (тумана, дыма, дымки) делает изображения более реалистичными. Функции моделирования тумана, дыма, загрязнений или просто эффекта присутствия воздуха можно найти в справочной системе по ключевому слову Fog.
Механизм anti-aliasing сглаживает неровные края линий, отображаемых на компьютерном дисплее при низком графическом разрешении. Anti-aliasing является стандартной техникой в компьютерной графике и заключается в изменении цвета точек вблизи границ изломов. Техника Gouraud-тени сглаживает тень трехмерного объекта для достижения тонких различий цветов на специфической поверхности.
Четвертая составляющая цветового кода RGBA носит название alpha. Alpha-смешивание позволяет комбинировать цвет обрабатываемого фрагмента с цветом точек, которые уже хранятся в буфере, моделируя тем самым прозрачность воздуха, стекла или другого материала. Этот эффект используют при демонстрации распределения поля внутри замкнутого объема. Достаточно дать пользователю возможность управлять степенью прозрачности границ, и он сможет рассматривать результаты вычислений (например, поверхности равного уровня искомого поля) внутри трехмерных объектов.
OpenGL не предоставляет разработчику команд для описания сложных моделей. В ней есть примитивные геометрические объекты: точки, линии и многоугольники. Разработчик должен сам сконструировать модели, основываясь на этих нескольких простых примитивах. Но есть библиотеки, например классы Open Inventor, которые реализуют более сложные модели. Разработчик может использовать эти библиотеки для построения своих собственных.
Конвейер OpenGL реализует процедурный, а не описательный подход к созданию изображения. Он выполняет функцию сервера, который обслуживает клиента — приложение, генерирующее последовательность команд. Коды сервера могут выполняться на другом компьютере, сервер может одновременно поддерживать несколько контекстов передачи OpenGL, а клиент может подключаться к любому из них. Однако система Windows полностью контролирует память, отводимую под буфер кадра (frame buffer). OpenGL может вступить в игру только после того, как Windows создаст и подготовит окно для передачи (rendering) изображения OpenGL.
Двойная буферизация
Двойная буферизация
В настоящий момент перерисовка изображения во время манипуляций мышью очень плохая, так как мы работаем с одним (front) буфером. Пора подключать второй. Вместо вызова glFlush; вставьте вызов функции auxSwapBuffers();
J- из другой библиотеки, которая, как вы помните, не документирована. Но этого мало — надо заменить волшебное слово SINGLE на не менее волшебное слово —DOUBLE. Местоположение вычислите самостоятельно. Поиск места вынуждает прокручивать в голове последовательность вызовов функций, что является полезным, а для многих и необходимым упражнением. После этого запустите приложение и отметьте, что управляемость кубика улучшилась, но при достаточно большом его повороте вокруг оси Y поворот вокруг оси X ведет себя так, как будто сама ось «повернута». Если вы поменяете порядок вызова двух функций вращения glRotated, то эффект останется, но проявит себя в симметричном варианте. Исправьте это, если хотите. Хорошая задача на сообразительность, так как не требует специфических знаний языка программирования, а только общих представлений о сути преобразований и возможностях библиотек OpenGL.
В примерах MSDN можно найти способ введения реакций на нажатия клавиш. Используем клавиши стрелок для смещения объекта в плоскости Z = const. Введите в функцию main декларацию 4 обработчиков:
auxKeyFunc(AUX_DOWN, KeyDown);
auxKeyFunc(AUX_UP, KeyUp);
auxKeyFunc(AUX_LEFT, KeyLeft);
auxKeyFunc(AUX_RIGHT, KeyRight);
Теперь по аналогии с мышиными событиями создайте самостоятельно функции обработки и меняйте внутри них те переменные, от которых зависит трансляция изображения. Например:
void _stdcall KeyDown()
{
gdTransY -=0.1; // Сдвигаем изображение вниз
}
void _stdcall KeyUp()
{
gdTransY += 0.1; // Сдвигаем изображение вверх
}
void _stdcall KeyLeft()
{
gdTransX -=0.1; // Сдвигаем изображение влево
}
void _stdcall KeyRight()
{
gdTransX +=0.1; // Сдвигаем изображение вправо
}
При тестировании результата обратите внимание на поведение изображения. Например, чем больше сдвиг вправо, тем лучше видна левая боковая грань. Кажется, что совместно с перемещением объекта он поворачивается. Но это не так. Эффект объясняется особенностями перспективной проекции.
Графика OpenGL
Графика OpenGL
Обзор возможностей библиотеки OpenGL
Перспективная проекция Вносим свет
Интерактивное управление положением и ориентацией Двойная буферизация
Строим икосаэдр
Как создать сферу
Массивы вершин, нормалей и цветов
В этом разделе мы научимся создавать трехмерные изображения с помощью функций библиотеки OpenGL, для того чтобы в следующей главе разработать Windows-приложение, которое можно рассматривать как инструмент просмотра результатов научных расчетов. Материал этого раздела позволит вам постепенно войти в курс дела и овладеть очень привлекательной технологией создания и управления трехмерными изображениями. Сначала мы рассмотрим основные возможности библиотеки OpenGL, затем научимся управлять функциями OpenGL на примере простых приложений консольного типа и лишь после этого приступим к разработке Windows-приложения.
Интерактивное управление положением и ориентацией
Интерактивное управление положением и ориентацией
Теперь хочется рассмотреть трехмерный объект с разных сторон. Удобнее это делать с помощью мыши. Документация MSDN содержит весьма скудные сведения относительно aux-функций, но в примерах все же можно найти какую-то информацию. Оказывается для введения реакции на мышиные события надо ввести в main следующие строки и, конечно, написать функции обработки
auxMouseFunc(AUX_LEFTBUTTON,AUX_MOUSELOC,OnLMouseMove); auxMouseFunc(AUX_RIGHTBUTTON,AUX_MOUSELOC,OnRMouseMove); auxMouseFunc(AUX_LEFTBUTTON,AUX_MOUSEDOWN,OnButtonDown); auxMouseFunc(AUX_RIGHTBUTTON,AUX_MOUSEDOWN,OnButtonDown);
Обратите внимание, что дих разделяет WM_MOUSEMOVE на две (в общем случае на три) кнопки. Это нам как раз подходит, так как мы хотим левой кнопкой вращать, а правой удалять-приближать (делать zooming) изображение. События отпускания кнопок нам не понадобились по причине того, что обработчики AUX_MOUSELOC (читай WM_MOUSEMOVE) вызываются только в случае, если соответствующие кнопки нажаты. Поэтому не нужно поднимать и отпускать флаг захвата объекта мышью. Именно это нам и нужно. Как легко, когда кто-то все продумал! Мы не делаем различия между нажатиями левой и правой кнопки, так как задача у них общая — запомнить текущие координаты указателя мыши. Вставьте декларации четырех функций, а затем приступим к их созданию. Так как сейчас мы не имеем классов для инкапсуляции переменных состояния мыши, то придется добавить глобальные переменные:
int giX, giY; // Текущая позиция указателя мыши
Тела глобальных функций обработки вы должны вставить до того места, в котором они вызываются. Алгоритм изменения параметров gdAngleX, gdAngleX и gdTransZ очевиден, но обратите внимание на детали. Например, как добывать координаты курсора мыши. Их присылает система, a AUX хранит их в структуре data, информацию о которой вы можете получить разве что в файле заголовков Glaux.h:
static void _stdcall OnButtonDown(AUX_EVENTREC *pEvent)
{
//====== Запоминаем координаты мыши
giX = pEvent->data[AUX_MOUSEX];
giY = pEvent->data[AUX_MOUSEY];
}
static void _stdcall OnLMouseMove(AUX_EVENTREC *pEvent)
{
//====== Узнаем текущие координаты
int x = pEvent->data[AUX_MOUSEX];
int у = pEvent->data[AUX_MOUSEY];
//====== Изменяем углы поворота пропорционально
//====== смещению мыши
gdAngleX += (у - giY)/10.f;
gdAngleY += (x - giX)/10.f;
//====== Запоминаем координаты мыши
giX = x; giY = у; >
Static void _stdcall OnRMouseMove(AUX_EVENTREC *pEvent)
int x = pEvent->data[AUX_MOUSEX];
int у = pEvent->data[AUX_MOUSEY] ;
//=====<= На сколько удалить или приблизить
double dx = (x - giX)/200.f;
double dy = (y - giY)/200.f;
//====== Удаляем или приближаем
gdTransZ += (dx + dy)/2.f;
//====== Запоминаем координаты мыши
giX = x; giY = y;
}
Запустите и опробуйте. Кубик должен управляться, но в обработке мышиных событий присутствует явная ошибка. Для того чтобы ее увидеть, нажмите правую кнопку и выведите курсор мыши за пределы окна влево. Изображение исчезло. один из слушателей наших курсов (Халип В. М. E-mail: viktor@mail.ru) самостоятельно нашел объяснение этому казусу и устранил дефект. Для того чтобы обнаружить его, вставьте в список директив препроцессора еще одну — #include <stdio.h>, а в функцию OnRMouseMove — вызов printf ("\n%d",x);. Теперь координата курсора мыши будет выводиться в текстовое окно консольного приложения. Повторите опыт с правой кнопкой и убедитесь в том, что при выходе за пределы окна (влево), координата х получает недопустимое значение (>65000). Для устранения дефекта достаточно заменить строки:
int x = pEvent->data[AUX_MOUSEX];
int у = pEvent->data[AUX_MOUSEY];
на
short x = pEvent->data[AUX_MOUSEX];
short у = pEvent->data[AUX_MOUSEY];
в функциях OnLMouseMove и OnRMouseMove. Теперь повторите опыт и убедитесь в том, что, переходя через границу окна, координата х изменяется монотонно и приобретает отрицательные значения. Чтобы быть последовательным, замените тип глобальных данных для хранения текущей позиции курсора мыши. Вместо int giX, giY; вставьте short giX, giY;. Объяснение эффекта мы оставляем читателю в качестве упражнения по информатике.
Интерполяция цвета
Интерполяция цвета
Вы можете запустить новый вариант приложения и убедиться в том, что он работает не хуже предыдущего. Кроме подготовки изображения (вызов DrawScene) мы внутри init включили возможность интерполяции цвета точек при заполнении полигона
glShadeModel (GL_SMOOTH) ;
Этого можно было и не делать, так как она включена по умолчанию. Но мы вставили код, с тем чтобы явно обозначить намерение исследовать эту возможность. Для того чтобы интерполяция начала действовать, надо задать разные цвета разным вершинам одного полигона. Уберите завершающий фрагмент функции DrawScene, где приведены коды непосредственного рисования куба, и вставьте вместо него следующий текст:
glPolygonMode (GL_FRONT_AND_BACK, GL_FILL) ;
glBegin (GL_QUADS) ;
//====== Обновляем генератор случайных чисел
srand (time (0) ) ;
//====== 6 граней куба
for (int i = 0; i < 6; i++) ( glNormalSdv (norm[i] ) ;
//====== 4 вершины одной грани
for (int j = 0; j < 4; j++)
{
//====== Задаем различные цвета
glColorSd (rand()%10/10.,
rand()%10/10., rand()%10/10.) ;
glVertex3fv(v[id[i] [ j ] ] ) ;
}
}
glEnd() ;
glEndList () ;
Включите в начало файла директиву препроцессора:
#include <time.h>
для того чтобы стала доступной функция timeQ. Она помогает настроить генератор псевдослучайных чисел так, чтобы при разных запусках программы получать различные комбинации цветов. Двойное деление на 10 (rand()%10/10.) позволяет масштабировать и нормировать компоненты цвета. Запустите и проверьте качество интерполяции цветов.
Использование списков
Использование списков
С кубиком быстро расправляется любой компьютер и видеокарта, а вот с более сложными объектами могут возникнуть затруднения, бороться с которыми можно с помощью нескольких приемов. Один из них — использование заранее откомпилированных списков команд OpenGL. Для иллюстрации этого приема создайте отдельную глобальную функцию:
void DrawScene()
{
//====== Создаем новый список команд OpenGL
glNewList(I,GL_COMPILE);
//====== Сюда поместите код, рисующий куб,
//====== начиная со строки
static float v[8][3] =
//====== и заканчивая
for (int j = 0; j < 4; j++)
glVertex3fv(v[id[i] [j] ] ) ;
}
glEnd() ;
glEndList () ;
}
Список рисующих команд OpenGL ограничивается операторными скобками вида:
glNewList(I, GL_COMPILE);
//====== Здесь располагаются команды OpenGL
glEndList () ;
Первый параметр glNewList (типа GLuint) идентифицирует список с тем, чтобы разработчик мог одновременно использовать несколько списков и вызывать их в нужные моменты времени по номеру. Вызов нашего (единственного) списка мы будем производить командой glCallList(l);. Команды, расположенные между строками giNewList(l, GL_COMPILE); и glEndListQ;, будут откомпилированы и сохранены в списке номер один. В функции перерисовки их следует просто воспроизвести. Для этого замените существующую версию функции OnDraw на новую:
void _stdcall OnDraw()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode (GL_MODELVIEW);
glLoadldentity ();
glTranslated(gdTransX, gdTransY, gdTransZ);
glRotated(gdAngleY, 0.,1.,0.);
glRotated(gdAngleX, 1. ,0 .,0 .);
//====== Воспроизводим команды из списка 1
glCallList (1);
auxSwapBuffers();
}
Вызов DrawScene можно осуществить внутри функции initQ, то есть один раз за время существования программного модуля:
void Init ()
{
glClearColor (1., 1., 1., 0.);
//====== Включаем интерполяцию цветов полигона
glShadeModel (GL_SMOOTH);
glShadeModel (GL_DEPTH_TEST) ;
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL),
glEnable(GL_LIGHTING);
glEnable(GL_LIGHTO);
glEnable(GL_COLOR_MATERIAL);
//====== Готовим сцену
DrawScene () ;
}
Как создать сферу
Как создать сферу
Для того чтобы из существующей заготовки — икосаэдра из двадцати граней — создать сферу, круглую, блестящую и без изъянов, нужно осуществить предельный переход, как в матанализе, бесконечно увеличивая число треугольников при бесконечном уменьшении их размеров. В дискретном мире нет места предельным переходам, поэтому вместо бесконечного деления надо ограничиться каким-то конечным числом и начать делить каждый из двадцати треугольников икосаэдра на все более мелкие правильные треугольники. Вычисление нормали при этом упрощается, так как при приближении к шару нормаль в каждой вершине треугольника приближается к нормали поверхности шара. А последняя равна нормированному вектору радиуса текущей точки. Алгоритм деления проиллюстрируем рисунком (Рисунок 6.3).
Как убирать внутренние линии
Как убирать внутренние линии
Каждой вершине по умолчанию присваивается булевский признак (флаг) того, что из нее может выходить видимое ребро (линия). Если надо отменить рисование линии, например скрыть тесселяцию вогнутого полигона, то можно снять флаг ребра командой glEdgeFlag(GL_FALSE); для текущей вершины. Затем можно вновь установить его, когда дело дойдет до ребер, которые должны быть видимы — команда glEdgeFlag(GL_TRUE);. Попробуйте самостоятельно вставить флаги ребер в следующем фрагменте программы так, чтобы скрыть линию соединения двух четырехугольников. Попробуйте затем заменить алгоритм так, чтобы изобразить ту же фигуру в виде одного полигона:
void _stdcall OnDraw()
{
glClear (GL_COLOR_BOFFER_BIT); glColorSd (1., 0.4, 1.);
//====== Вогнутый шестиугольник, но мы зададим его
//====== в виде двух четырехугольников
float с[6][3] =
{
200. 200.,0.,
200. 100.,0.,
250. 20.,0.,
300. 100.,0.,
300. 200.,0.,
250. 100.,0.,
};
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glBegin(GL_QUADS);
glVertex3fv(c[5])
glVertex3fv(c[0])
glVertex3fv(c[l])
glVertex3fv(c[2])
glVertex3fv(c[5])
glVertex3fv(c[2])
glVertex3fv(c[3])
glVertex3fv(c[4])
glEnd();
glFlush ();
}
Контекст передачи изображения
Контекст передачи изображения
Окно OpenGL имеет свой собственный формат пикселов. Необходимым условием ее работы является установка pixel-формата экранной поверхности в контексте устройства HDC, а следовательно, и в окне, с которым он связан. Формат устанавливается один раз, повторная установка недопустима, так как может привести к сбоям в работе подсистемы управления окнами (Windows Manager). После установки формата (вызов SetPixelFormat) следует создать контекст передачи изображения OpenGL, описатель которой имеет тип HGLRC. Контекст передачи (rendering context) создается функцией wglCreateContext с учетом выбранного формата пикселов. Контекст передачи изображения — это связь OpenGL с Windows. Создание этого контекста требует, чтобы обычный контекст существовал и был явно указан в параметре wglCreateContext. Контекст HGLRC использует тот же формат пикселов, что и HDC.
Несмотря на сходство, эти контексты различны. HDC содержит информацию относящуюся к функциям GDI, a HGLRC — к функциям OpenGL. Поток, вызывающий функции OpenGL, должен предварительно объявить контекст передачи текущим (current). Иначе вызовы не будут иметь эффекта. Уничтожать контекст передачи надо после отсоединения его от потока. Несколько контекстов передачи могут одновременно рисовать в окне OpenGL, но только один из них (тот, который ассоциирован с HDC) может быть текущим или активным в потоке.
Для описания формата пикселов экранной поверхности в OpenGL используется структура PIXELFORMATDESCRIPTOR. Прежде всего pixel format — это описание цветового режима, действующего в данном окне. Например, если видеокарта может работать в режиме передачи 256 цветов, то для кодирования цвета каждого пиксела в этом режиме необходимо иметь 8 бит памяти. В этом случае говорят о 8-битовой глубине поверхности рисования или окна OpenGL. Иногда в таком случае говорят о 8-битовой глубине цвета. Существуют режимы с 15-битовой глубиной (32 768 цветов), 16-битовой (65 536 цветов), 24-битовой (16 миллионов цветов). Выбор формата зависит от возможностей карты и намерений разработчика. Кроме глубины цвета к pixel-формату относятся такие настройки, как:
схема образования цвета (RGBА или на основе палитры);
количество бит для буфера глубины, то есть буфера Z-координат изображения (ось Z считается направленной из глубины экрана к наблюдателю);
поддержка регулировки прозрачностью (alfa) и др.
Вы можете выбрать один из более чем 20 готовых pixel-форматов или задать произвольную комбинацию параметров и попросить найти ближайшую возможную ее реализацию. Microsoft GDI-реализация OpenGL вносит свои коррективы в возможные варианты реализации pixel-формата, а аппаратная поддержка меняется в зависимости от производителя и может значительно расширить его возможности. Каждое окно OpenGL должно иметь свой собственный pixel-формат.
По умолчанию Windows не вырезает (в смысле перерисовки) дочерние окна из клиентской области родительского окна, поэтому при создании окнам OpenGL следует задать бит стиля WS_CLIPCHILDREN. В этом случае система не позволяет рисовать родительскому GDI-окну в пределах дочернего окна OpenGL. Несколько окон OpenGL, каждое со своим форматом, могут быть отображены одновременно, поэтому необходимо установить еще один бит стиля WS_CLIPSIBLINGS, чтобы предотвратить рисование в окне соседа. Для окон OpenGL недопустим стиль CS_PARENTDC (CM. MSDN).
Конвейер передачи OpenGL
Конвейер передачи OpenGL
Команды OpenGL претерпевают одинаковый порядок обработки, проходя через последовательность стадий, называемых конвейером обработки OpenGL (processing or rendering pipeline). Схема конвейера приводится во многих источниках, приведем ее и мы (Рисунок 6.1), для того чтобы не отсылать читателя к другим книгам. Ниже следует краткое описание его основных блоков.
Массивы вершин нормалей и цветов
Массивы вершин, нормалей и цветов
Неэффективность алгоритма последовательного рисования большого числа примитивов не является тайной для тех, кто имеет дело с трехмерной графикой. Поэтому в технологии OpenGL существует ряд приемов (поддержанных функциями), которые основаны на использовании заранее заготовленных массивов, а также списков команд OpenGL. Они значительно повышают эффективность работы конвейера при передаче (rendering) изображений, составленных из десятков и сотен тысяч примитивов. Например, функция glDrawElements позволяет задать геометрические примитивы экономичным способом, то есть с минимальными затратами на вызовы функций. До сих пор мы вызывали в среднем 4-5 функций для каждого треугольника. При этом многократно повторялись, так как вершины, общие для смежных треугольников, задавались не один раз. Массивы величин, ассоциируемых с вершинами (координаты, нормали, цвета и другие), могут быть сформированы заранее и использованы при описании геометрии с помощью массива индексов. Функция glDrawElements требует в качестве одного из параметров массив индексов вершин полигонов. Вот ее прототип:
void glDrawElements (GLenum mode, GLsizei count,
GLenum type, const GLvoid *indices);
Функция конструирует count элементов типа mode. Параметр indices должен содержать адрес массива индексов, который формируется заранее. Параметр type определяет тип элементов массива indices. Он может принимать одно из трех фиксированных значений: GL_UNSIGNED_BYTE (используется 8-битовый индекс), GL_UNSIGNED_SHORT (16-биТНЫЙ ИНДСКС), GL_UNSIGNED_INT (32-биТНЫЙ). Характерной особенностью рассматриваемой технологии является то, что величины, ассоциируемые с каждой вершиной примитива, могут храниться в разных массивах или в одном массиве структур с разными полями. Они задаются с помощью 6 функций:
GINormalPointer — задает адрес массива нормалей в вершинах;
GlColorPointer — задает адрес массива цветов, связанных с вершинами;
GlTexCoordPointer — задает адрес массива координат текстуры материала, задаваемой в вершинах;
GlEdgeFlagPointer — задает адрес массива флагов видимости линий, исходящих из вершины;
GllndexPointer — задает адрес массива цветовых индексов вершин в режиме цветовой палитры, а не RGBA.
Другой массив индексов — indices, определяет порядок выбора элементов из этих шести массивов. Но этого мало — надо произвести еще некоторые настройки в машине состояний OpenGL. Для перевода ее в режим использования массивов надо несколько раз вызвать функцию glEnableClientstate. Каждый вызов включает один из шести рассмотренных режимов. Только после этого функция glDrawElements способна эффективно задать сразу все примитивы. Например, вызов:
glEnableClientState(GL_VERTEX_ARRAY);
включает режим использования массива координат вершин, а вызов этой же функции с параметром GL_NORMAL_ARRAY включает использование массива нормалей.
Совместно с командой glDrawElements обычно используют тот способ повышения эффективности отображения примитивов, который мы уже используем. Речь идет о паре функций: glNewList, glEndList. Все команды OpenGL, заданные между вызовами этих двух функций, оптимизируются, компилируются (по выбору) и запоминаются в отдельном нумеруемом списке.
Обзор возможностей библиотеки OpenGL
Обзор возможностей библиотеки OpenGL
Читатель, наверное, знает, что OpenGL это оптимизированная, высокопроизводительная графическая библиотека функций и типов данных для отображения двух-и трехмерной графики. Стандарт OpenGL был утвержден в 1992 г. Он основан на библиотеке IRIS GL, разработанной компанией Silicon Graphics (www.sgi.com). OpenGL поддерживают все платформы. Кроме того, OpenGL поддержана аппа-ратно. Существуют видеокарты с акселераторами и специализированные SD-кар-ты, которые выполняют примитивы OpenGL на аппаратном уровне.
Материал первой части этого урока навеян очень хорошей книгой (доступной в online-варианте) издательства Addison-Wesley «OpenGL Programming Guide, The Official Guide to Learning OpenGL». Если читатель владеет английским языком, то мы рекомендуем ее прочесть.
Ограничения Microsoft
Ограничения Microsoft
К сожалению, Microsoft-реализация OpenGL имеет ряд ограничений, которые не дают в полной мере использовать всю мощь библиотек. Перечислим те из них, которые приведены в документации MSDN.
Изображение OpenGL можно вывести на печать только с помощью метафайлов (списка рисующих команд GDI). При этом надо учитывать специфику, описанную в документации.
Нет поддержки стереоскопических изображений.
OpenGL и GDI-графику можно совмещать только в окне с одинарной буферизацией.
Windows имеет одну системную цветовую палитру, которая применяется ко всему экрану, поэтому окно OpenGL не может иметь собственной аппаратной палитры, но может иметь собственную логическую палитру.
Окно OpenGL не поддерживает динамический обмен данными (DDE), обмен с помощью механизма Clipboard и OLE. Однако существуют обходные пути для использования операций с Clipboard
Библиотеки классов, такие как Volumizer и Open Inventor, которые обеспечивают более высокий уровень конструирования 3-D графики, не включены в состав Microsoft-реализации OpenGL. Это, на мой взгляд, является очень серьезным недостатком.
OpenGL — автомат с конечным числом состояний
OpenGL — автомат с конечным числом состояний
OpenGL работает по принципу конечного автомата, то есть автомата, который в каждый момент времени находится в одном из состояний, принадлежащих конечному множеству допустимых значений. В документации вы можете встретить в применении к OpenGL термины state machine (конечный автомат) и assembly line (конвейер). Некоторые команды (вызовы функций OpenGL) переводят автомат в различные состояния или режимы, которые остаются неизменными до тех пор, пока не придет следующая команда изменения состояния. Текущий цвет, как вы видели, является одним из состояний. Другими состояниями являются:
тип проективных или обзорных преобразований (projection and viewing transformations);
режимы рисования полигонов;
режимы упаковки пикселов;
расположение источников света и свойства материалов.
Многие переменные, определяющие состояния, переключаются с помощью функций glEnable (включить) или gioisable (выключить). Каждая переменная состояния или режим имеет значение по умолчанию, и в любой точке программы вы можете узнать текущее состояние. Обычно для этой цели используется одна из 6-ТИ команд: glGetBooleanv, glGetDoublev, glGetFloatv, glGetlntegerv, glGetPointerv или glisEnabled. Выбор зависит от типа данных, которые задают состояние. Некоторые переменные состояния (state variables) заполняются более специфичными командами, например: glGetLight*, glGetError, glGetPolygonStipple. Множество состояний можно сохранить в стеке атрибутов командами glPushAttrib или glPushClientAttrib. Обычно так делают для того, чтобы временно изменить что-то, а затем восстановить состояния с помощью одной из команд: glPopAttrib, glPopClientAttrib.
Основные этапы
Основные этапы
Для того чтобы запомнить основные этапы обработки, повторим ключевые моменты.
Параллельная линия обработки исходных данных задает непосредственно пикселы.
Примитивы, заданные в трехмерном пространстве, преобразуются в двухмерное изображение с помощью растеризации.
Каждая точка уже двухмерного изображения характеризуется цветом, глубиной (значением координаты Z) и данными о текстуре. Такая точка вместе с сопутствующей информацией называется фрагментом.
Фрагмент изменяет соответствующий ему пиксел в буфере кадра, если он проходит пять тестов.
Каждая вершина вместе с характеризующими ее данными обрабатывается конвейером OpenGL независимо и последовательно. Это означает, что каждый примитив будет полностью изображен до того, как выполнится следующая команда.
Более подробную функциональную схему конвейера вы можете увидеть в разделе MSDN: Platform SDK/OpenGL/Overview/Introduction to OpenGL/OpenGL Processing Pipeline.
Перспективная проекция
Перспективная проекция
В ортографической проекции .(giuOrtho2D) мы, в сущности, создавали двухмерные изображения в плоскости z = 0. В других типах проекций (gldrtho и gluPerspective) можно создавать трехмерные изображения. Эффект реального трехмерного пространства достигается в проекции с учетом перспективы. Теперь мы будем пользоваться только этим режимом передачи. Другой режим glOrtho вы опробуете самостоятельно, так как я не вижу какой-либо интересной сферы его применения. Вставьте в обработчик WM_SIZE вместо строки:
gluOrtho2D (0., double (w), 0., double(h) ) ;
строку:
gluPerspective(45., double(w)/double(h), 1., 100.);
В OpenGL для обозначения видимого объема используется термин frustum. Он имеет латинское происхождение и примерно означает «отломанная часть, кусок».
Frustum задается шестью плоскими границами типа (min, max) для каждой из трех пространственных координат. В перспективном режиме просмотра frustum — это усеченная пирамида, направленная на наблюдателя из глубины экрана. Все детали сцены, которые попадают внутрь этого объема, видны, а те, что выходят за него, — отсекаются конвейером OpenGL. Другой режим просмотра — ортографический, или режим параллельной проекции, задается с помощью функции glOrtho. Он не учитывает перспективу, то есть при увеличении (удалении) координаты Z объекта от точки, в которой располагается наблюдатель, размеры объектов и углы между ними не изменяются, что напоминает плоские проекции объекта. Первый параметр функции gluPerspective задает угол перспективы (угол обзора). Чем он меньше, тем больше увеличение. Вспомните школьные объяснения работы телескопа или бинокля, где были настройки фокусного расстояния, определяющего угол зрения. Последние два параметра задают переднюю и заднюю грани видимого объема или frustum'a. Он определяет замкнутый объем, за пределами которого отсекаются все элементы изображения. Смотри иллюстрации в MSDN / Periodicals / Periodicals 96 / Microsoft System Journals/November / OpenGL Without the Pain. Боковые грани фрустума определяются с учетом дисбаланса двух размеров окна (отношения double(w) / double(h)). Мы вычисляем его и подаем на вход функции в качестве второго параметра.
Вспомните и найдите функцию, в которой мы задавали размеры окна, и увеличьте вертикальный размер до 500, так как далее мы собираемся изображать более крупные объекты. Введите определения новых глобальных переменных:
//====== Углы поворотов изображения вокруг осей X и Y
double gdAngleX, gdAngleY; //====== Сдвиги вдоль координат
double gdTransX, gdTransY, gdTransZ = -4.;
С их помощью мы будем транслировать (перемещать) изображения в трехмерном пространстве и вращать их вокруг двух осей. Включите учет глубины, вставив вызов
glEnable(GL_DEPTH_TEST);
в функцию Init. Туда же вставьте установку режима заполнения полигонов
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
и уберите строку, задающую текущий цвет вершин
glColorSd (1., 0.4, 1.);
так как мы теперь будем задавать его в другом месте. При подготовке окна OpenGL и формата его пикселов надо установить бит AUX_DEPTH — учет буфера глубины. Замените существующий вызов функции auxlnitDisplayMode на: auxInitDisplayMode (AOX_SINGLE I AUX_RGB I AUX_DEPTH);
В функции перерисовки, приведенной ниже, мы создадим куб, координаты которого будем преобразовывать с помощью матрицы моделирования. Порядок работы с этой матрицей таков:
После этого текущая (единичная) матрица последовательно домножается справа на матрицы преобразования системы координат, которые формируются с помощью команд glTranslate* (сдвиги), glRotate* (вращения) или glScale* (растяжения-сжатия).
Наконец, команды glVertex* генерируют вершины примитивов, то есть координатные векторы точек трехмерного пространства. Векторы умножаются (справа) на текущую матрицу моделирования и тем самым претерпевают такие преобразования, чтобы соответствовать желаемому местоположению и размерам в сцене OpenGL.
Предположим, например, что текущая (current) матрица С размерностью 4x4 равна единичной С = 1 и поступает команда glTranslated (dx, dy, dz);. Эта команда создает матрицу сдвига Т и умножает ее справа на текущую (единичную) матрицу (С = I*Т). Затем она вновь записывает результат в текущую матрицу С. Теперь текущая матрица приняла вид:
1 | 0 | 0 | dx | ||
C= | 0 | 1 | 0 | dy | |
0 | 0 | 1 | dz | ||
0 | 0 | 0 | 1 |
Если после этого дать команду glVertexSd (x, у, z); то координаты точки (х, у, z) преобразуются по правилам умножения матрицы на вектор:
1 | 0 | 0|| | dx | || | x| | |x | +dx| | ||
0 | 1 | 0|| | dy | || | y| | = | |y | +dy| | |
0 | 0 | 1|| | dz | || | z| | |z | +dz| | ||
0 | 0 | 0|| | 1 | || | 1| | |1 |
Примечание 1
Примечание 1
Вы должны помнить, что вершины всех примитивов в OpenGL заданы 4-ком-— понентным вектором (х, у, z, w). По умолчанию нормирующий компонент w-1. При работе с двухмерными изображениями мы для всех вершин задаем координату z = 0. Обратите внимание на то, как четвертый компонент w помогает производить преобразования, в нашем случае сдвиг, а команда glTranslate* учитывает координаты сдвигов вдольтрех пространственных осей (dx, dy, dz).
Команды вращения glRotate* и растяжения-сжатия glScale* действуют сходным образом. В функции onDraw, приведенной ниже, начальный поворот и последующие вращения вокруг оси Y осуществляются вызовом glRotated (gdAngleY, 0., l., 0.);. Аналогичный вызов glRotated (gdAngleX, 1., 0., 0.); вращает все точки примитивов вокруг оси X:
void _stdcall OnDraw()
{
glClear(GL_COLOR_BOFFER_BIT I GL_DEPTH_BUFFER_BIT);
//== Будем пользоваться услугами матрицы моделирования glMatrixMode <GL_MODELVIEW);
glLoadldentity ();
//=== Задаем смещение координат точек будущих примитивов glTranslated(gdTransX, gdTransY, gdTransZ);
//===Задаем вращение координат точек будущих примитивов
glRotated(gdAngleY, 0.,1.,0.);
glRotated(gdAngleX, 1.,0.,0.);
//====== Координаты точек куба (центр его в нуле)
static float v[8][3] =
{
-1, 1.,-1., //4 точки задней грани задаются
1., 1., -1., //в порядке против часовой стрелки
1-, -1-, -1.,
-1, -1., -1.,
-1, 1,, 1., //4 фронтальные точки
-1-, -1., 1.,
1, -1., 1.,
1, 1., 1.
};
//====== 6 нормалей для 6-ти граней куба
static double norm[6][3] =
{
0., 0., -1., // Rear
0., 0., 1., // Front
-1., 0., 0., // Left
1., 0., 0., // Right
0., 1., 0., // Top
0., -1., 0. // Bottom
};
//====== Индексы вершин
static GLuint id[6][4] =
{
0,1,2,3,// Rear (обход CCW - counterclockwise)
4,5,6,7, // Front
0,3,5,4, // Left
7,6,2,1, // Right
0,4,7,1, // Top
5,3,2, 6, // Bottom
};
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glColorSd (1., 0.4, 1.);
glBegin(GL_QUADS);
//====== Долго готовились - быстро рисуем
for (int i = 0; i < 6; i++)
{
glNormal3dv(norm[i]) ;
for (int j = 0; j < 4; j++)
glVertex3fv(v[id[i] [j]]);
}
glEnd() ;
glFlush ();
}
Запустите и отладьте приложение. Вы должны увидеть совсем плоский квадрат, несмотря на обещанную трехмерность объекта. Пока ничего вразумительного, никакого трехмерного эффекта. Закомментируйте или удалите (или измените на GL_SMOOTH) настройку glShadeModel (GL_FLAT), так как теперь мы хотим интерполировать цвета при изображении полигонов. Это работает при задании разных цветов вершинам. Попробуйте задать всем вершинам разные цвета.
Попробуйте покрутить изображение, изменяя значения переменных gdAngleX, gdAngleY. Например, вместо нулевых значений, присваиваемых глобальным переменным по умолчанию, задайте:
double gdAngleX=20, gdAngleY=20;
Посмотрите в справке смысл всех параметров функции glRotated и опробуйте одновременное вращение вокруг двух осей, задав большее число единиц в качестве параметров. Позже мы автоматизируем процесс сдвигов и вращений, а сейчас, пока мы не умеем реагировать на сообщения мыши, просто измените значение какого-либо угла поворота и запустите музыку. Объясните результаты. Попробуйте отодвинуть изображение, изменив регулировку gdTransZ. Объясните знак смещения.
Подготовка окна
Подготовка окна
Подготовку контекста передачи OpenGL надо рассматривать как некий обязательный ритуал, в котором порядок действий давно определен и без которого нельзя начинать творческую работу по созданию сцены OpenGL. Стоит где-то промахнуться, и вы увидите молчаливый белый экран. Сначала надо подготовить окно так, чтобы вызовы функций OpenGL начали работать. В этой процедуре выделяют следующие шаги:
обработка сообщения WM_ERASEBACKGROUND и отказ от стирания фона;
установка pixel-формата;
создание контекста устройства (НОС) и контекста передачи (HGLRC);
специфическая обработка сообщения WM_SIZE;
обработка сообщения WM_PAINT;
освобождение контекстов при закрытии окна.
Чтобы использовать функции библиотеки OpenGL в вашем приложении, надо убедиться, что в системном каталоге Windows присутствуют модули OpenGL32.dll и GLU32.dll. Они должны быть там, так как компания Silicon Graphics (авторы пакета OpenGL) постаралась, чтобы поддержка OpenGL на платформе Windows была максимально доступна и достаточно надежна. Однако хочу предупредить, что я встречал системы, в которых контекст передачи (rendering context) OpenGL работает ненадежно — появляются пятна пробелов и задержка перерисовки, если работа идет не в полноэкранном режиме. Если это есть, то должно проявляться при запуске любой программы, использующей OpenGL. Причина, видимо, в драйвере видеопамяти.
Типы данных
OpenGL использует свои собственные типы данных, которые должны соответствовать аналогичным типам той платформы, на которой библиотека установлена. В Microsoft-реализации соответствие типов задано в файле заголовков GL.H так, как показано ниже. Эта таблица понадобится вам при анализе примеров и при разработке собственного кода:
typedef unsigned int GLenum;
typedef unsigned char GLboolean;
typedef unsigned int GLbitfield;
typedef signed char GLbyte;
typedef short GLshort;
typedef int GLint;
typedef int GLsizei;
typedef unsigned char GLubyte;
typedef unsigned short GLushort;
typedef unsigned int GLuint;
typedef float GLfloat;
typedef float GLclampf;
typedef double GLdouble;
typedef double GLclampd;
typedef void GLvoid;
Подключаемые библиотеки
Подключаемые библиотеки
Microsoft-реализация OpenGL включает полный набор команд OpenGL, то есть глобальных функций, входящих в ядро библиотеки OPENGL32.LIB и имеющих префикс gl (например, glLineWidth). Заметьте, что функции из ядра библиотеки имеют множество версий, что позволяет задать желаемый параметр или настройку любым удобным вам способом. Посмотрите справку по функциям из семейства glColor*. Оказывается, что задать текущий цвет можно 32 способами. Например, функция:
void glColorSb(GLbyte red, GLbyte green, GLbyte blue);
определяет цвет тремя компонентами типа GLbyte, а функция
void glColor4dv(const GLdouble *v) ;
задает его с помощью адреса массива из четырех компонентов.
С учетом этих вариантов ядро библиотеки содержит более 300 команд. Кроме того, вы можете подключить библиотеку утилит GLU32.LIB, которые дополняют основное ядро. Здесь есть функции управления текстурами, преобразованием координат, генерацией сфер, цилиндров и дисков, сплайновых аппроксимаций кривых и поверхностей (NURBS — Non-Uniform Rational B-Spline), а также обработки ошибок. Еще одна, дополнительная (auxiliary) библиотека GLAUX.LIB позволяет простым способом создавать Windows-окна, изображать некоторые SD-объекты, обрабатывать события ввода и управлять фоновым процессом. К сожалению, эта библиотека не документирована. Компания Microsoft не рекомендует пользоваться ею для разработки коммерческих проектов, так как она содержит код цикла обработки сообщений, в который невозможно вставить обработку других произвольных сообщений.
Примечание 1
Примечание 1
Тип GLbyte эквивалентен типу signed char, a GLdouble — типу double. Свои собственные типы используются в целях упрощения переносимости на другие платформы. Список типов OpenGL мы приведем ниже. Четвертый компонент цвета определяет прозрачность цвета, то есть способ смешивания цвета фона с цветом изображения. Некоторые команды OpenGL имеют в конце символ v, который указывает, что ее аргументом должен быть адрес массива (вектора). Вектор в математике — это последовательность чисел (координат), единственным образом задающих элемент векторного пространства. Многие команды имеют несколько версий, позволяя в конечном счете задать вектор разными способами.
Около двадцати Windows GDI-функций создано специально для работы с OpenGL. Большая часть из них имеет префикс wgl (аббревиатура от Windows GL). Эти функции являются аналогами функций с префиксом glx, которые подключают OpenGL к платформе X window System. Наконец, существует несколько Win32-функций для управления форматом пикселов и двойной буферизацией. Они применимы только для специализированных окон OpenGL.
Примитивы OpenGL
Примитивы OpenGL
Моделью или объектом в OpenGL называется структура в памяти, конструируемая из геометрических примитивов: точек, линий и полигонов, которые, в свою очередь, задаются своими вершинами (vertices). Из этих моделей OpenGL создает изображение в специально подготовленном окне. Процесс создания и демонстрации изображения называется передачей (rendering) изображения OpenGL. Конечным изображением является множество пикселов — мельчайших видимых элементов экранной поверхности. Информация о цвете пикселов размещена в памяти в виде битовых плоскостей (bitplanes). Так называется область памяти, которая содержит только один бит информации обо всех пикселах окна. В совокупности плоскости составляют буфер кадра (framebuffer), который содержит информацию, необходимую для того, чтобы дисплей отобразил все пикселы окна OpenGL.
OpenGL изображает графические примитивы (точки, сегменты линий или многоугольники), используя при этом множество независимо управляемых режимов (modes). Для задания примитивов, установки режимов и выполнения других операций необходимо вызывать функции OpenGL, или, как принято говорить, давать последовательность команд OpenGL Примитивы задаются своими вершинами, то есть точками трехмерного пространства. Кроме координат с каждой вершиной ассоциируются такие данные, как цвет, направление нормали (перпендикуляра), параметры текстуры и флаги границы (edge flags). Текстурами называются готовые bitmap-изображения, которые накладываются на многоугольники каркаса модели и вносят в нее эффект поверхности реального материала.
Разбиение сферы на треугольники
Рисунок 6.4. Разбиение сферы на треугольники
Мы будем управлять степенью дискретизации сферы с помощью двух чисел: количества колец (gnRings) и количества секций (gnSects). Они определяют как полное количество вершин, так и треугольников. Если глобально зададим переменные:
const UINT gnRings = 20; // Количество колец (широта)
const UINT gnSects = 20; // Количество секций (долгота),то, так как каждый прямоугольник разбит на два треугольника, общее количество треугольников будет:
const UINT gnTria = (gnRings+1) * gnSects * 2;
//===Нетрудно подсчитать и общее количество вершин:
const UINT gnVert = (gnRings+1) * gnSects + 2;
Мы уже, по сути, начали писать код, поэтому создайте новый файл Sphere.срр и подключите его к проекту, а предыдущий файл OG.cpp отключите. Эти действия производятся так:
Переведите фокус на строку Console того же окна и, вызвав контекстное меню, дайте команду Add New Item.
Выберите шаблон C++ File (.срр) и, задав имя файла Sphere.срр, нажмите ОК.
Введите в него директивы препроцессора, которые нам понадобятся, а также объявления некоторых констант:
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <time.h>
#include <windows.h>
#include <gl\gl.h>
#include <gl\glu.h>
#include <gl\glaux.h>
const UINT gnRings = 40; // Количество колец (широта)
const UINT gnSects = 40; // Количество секций (долгота)
//====== Общее количество треугольников
const UINT gnTria = (gnRings+1) * gnSects * 2;
//====== Общее количество вершин
const UINT gnVert = (gnRings+1) * gnSects + 2;
//====== Два цвета вершин
const COLORREF gClrl = RGB(0, 255, 0);
const COLORREF gClr2 = RGB(0, 0, 255);
const double gRad = 1.5; // Радиус сферы
const double gMax =5.; // Амплитуда сдвига
const double PI = atan(1.)*4,; // Число пи
Класс точки в 3D
С каждой вершиной, как вы помните, связано множество параметров, определяющих качество изображения OpenGL. Мы остановимся на наборе из трех величин: координаты вершины, вектор нормали и цвет. Так как вектор нормали и координаты можно задать с помощью двух объектов одного и того же типа (три вещественных переменных х, у, z), то целесообразно ввести в рассмотрение такое понятие, как точка трехмерного пространства. И воплотить его в виде класса CPoint3D, который инкапсулирует функциональность такой точки. Введите определение класса в конец файла Sphere. срр:
//====== Точка 3D-пространства
class CPointSD
{
public: float x, у, z; // Координаты точки
// ====== Конструктор по умолчанию
CPoint3D () { х = у = z = 0; ) //====== Конструктор с параметрами
CPointSD (double cl, double c2, float c3)
{
x = float (cl) ;
z = float(c2) ;
у = float(c3) ;
}
//====== Операция присвоения
CPoint3D& operator= (const CPoint3D& pt)
{
x = pt.x;
z = pt . z ;
У = Pt.y;
return *this;
//====== Операция сдвига в пространстве
CPoint3D& operator+= (const CPoint3D& pt)
{
x += pt.x;
y += Pt.y;
z += pt . z ;
return * this ;
}
//====== Конструктор копирования
CPointSD (const CPoint3D& pt)
{
*this = pt;
}
};
Обратите внимание на тот факт, что конструктор копирования использует код уже существующей операции присвоения. Имея в своем распоряжении класс CPointSD, мы можем создать еще один тип данных — структуру, поля которой объединяют все величины, связанные с вершиной треугольника. Массив данных такого типа будет хранить информацию обо всех вершинах изображения и при этом не будет повторений:
//====== Данные о вершине геометрического примитива
struct VERT
{
CPointSD v; // Координаты вершины
CPoiivt3D n; // Координаты нормали
DWORD с; // Цвет вершины
};
Введите эту декларацию после кода, определяющего CPoint3D. Как было отмечено, функция glDrawElements в качестве параметра требует задать массив индексов вершин. В соответствии с этими индексами вершины треугольников будут выбираться из общего массива вершин. Порядок следования индексов зависит от порядка обхода вершин при задании треугольников. Как вы помните, он должен идти против часовой стрелки, если смотреть на примитив с конца внешней нормали. В этом случае знак нормали соответствует формулам векторной алгебры,!: которые мы уже рассматривали.
Будет удобно, если мы сначала создадим структуру, которая объединяет три индекса вершин одного треугольника. Тогда массив структур такого типа сможет играть роль массива индексов, требуемого функцией glDrawElements. Введите следующее описание в продолжение файла:
struct TRIA
{
//====== Индексы трех вершин треугольника,
//====== выбираемых из массива вершин типа VERT
//====== Порядок обхода — против часовой стрелки
int i1;
int i2;
int i3;
};
Далее нам понадобятся две глобальные неременные типа CPointSD, с помощью *":' которых мы будем производить анимацию изображения сферы. Анимация, а также различие цветов при задании вершин треугольников позволят более четко передать трехмерный характер изображения. Наличие освещения подвижного объекта также заметно увеличивает его реалистичность. При создании програм-| мы мы обойдемся одним файлом, поэтому новые объявления продолжайте вставлять в конец файла Sphere.срр:
//====== Вектор углов вращения вокруг трех осей ?
CPointSD gSpin; //====== Вектор случайной девиации вектора gSpin
CPointSD gShift;
При каждой смене буферов (перерисовке изображения) мы будем вращать изоб- ; ражение сферы вокруг всех трех осей на некоторый векторный квант gshif t. Для того чтобы вращение было менее однообразным, введем элемент случайности. Функция Rand, приведенная ниже, возвращает псевдослучайное число в диапазоне (-х, х). Мы будем пользоваться этим числом при вычислении компонентов вектора gshif t. Последний, воздействуя на другой вектор gSpin, определяет новые значения трех углов вращения, которые функция glRotate использует для задания очередной позиции сферы:
inline double Rand(double x)
{
//====== Случайное число в диапазоне (-х, х)
return х - (х + х) * rand() / RAND_MAX;
}
Учитывая сказанное, можно создать алгоритм перерисовки:
void _stdcall OnDraw()
{
glClear(GL_COLOR_BUFFER_BIT) ;
//=== Сейчас текущей является матрица моделирования
glLoadldentityО;
//====== Учет вращения
glRotated(gSpin.х, 1., О, 0.) ;
glRotated(gSpin.y, 0., 1., 0.);
glRotated(gSpin.z, 0., 0., 1.) ;
//====== Вызов списка рисующих команд
glCallList(1);
//====== Подготовка следующей позиции сферы
gSpin += gShift;
//===== Смена буферов auxSwapBuffers();
}
Подготовка сцены
Изображение сферы целесообразно создать заранее (в функции init), а затем воздействовать на него матрицей моделирования, коэффициенты которой изменяются в соответствии с алгоритмом случайных девиаций вектора вращения. При разработке кода функции init надо учесть специфику работы с функцией glDrawElements, которая обсуждалась выше. Кроме того, здесь мы производим установку освещенности, технологию и детали которой можно выяснить в сопровождающей документации (MSDN). Введите следующие коды функции инициализации и вставьте их до функции перерисовки:
void Init ()
{
//=== Цвет фона (на сей раз традиционно черный)
glClearColor (0., 0., 0., 0.);
//====== Включаемаем необходимость учета света
glEnable(GL_LIGHTING);
//=== Включаемаем первый и единственный источник света
glEnable(GL_LIGHT());
//====== Включаем учет цвета материала объекта
glEnable(GL_COLOR_MATERIAL);
// Вектор для задания различных параметров освещенности
float v[4] =
{
0.0Sf, 0.0Sf, 0.0Sf, l.f
};
//=== Сначала задаем величину окружающей освещенности glLightModelfv(GL_LIGHT_MODEL_AMBIENT, v);
//====== Изменяем вектор
v[0] = 0.9f; v[l] = 0.9f; v[2] = 0.9f;
//====== Задаем величину диффузной освещенности
glLightfv(GL_LIGHTO, GL_DIFFUSE, v) ;
//======= Изменяем вектор
v[0] = 0.6f; v[l] = 0.6f; v[2] = 0.6f;
//====== Задаем отражающие свойства материчала
glMaterialfv(GL_FRONT, GL_SPECULAR, v);
//====== Задаем степень блесткости материала
glMateriali(GL_FRONT, GL_SHININESS, 40);
//====== Изменяем вектор
v[0] = O.f; v[l] = O.f; v[2] = l.f; v[3] = O.f;
//====== Задаем позицию источника света
glLightfv(GL_LIGHTO, GL_POSITION, v);
//====== Переключаемся на матрицу проекции
glMatrixMode(GL_PROJECTION); glLoadldentity();
//====== Задаем тип проекции
gluPerspective(45, 1, .01, 15);
//=== Сдвигаем точку наблюдения, отодвигаясь от
//=== центра сцены в направлении оси z на 8 единиц
gluLookAt (0, 0, 8, 0, 0, 0, 0, 1, 0) ;
//====== Переключаемся на матрицу моделирования
glMatrixMode(GL_MODELVIEW);
//===== Включаем механизм учета ориентации полигонов
glEnable(GL_CULL_FACE);
//===== Не учитываем обратные поверхности полигонов
glCullFace(GL_BACK);
//====== Настройка OpenGL на использование массивов
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
//====== Захват памяти под динамические массивы
VERT *Vert = new VERT[gnVert];
TRIA *Tria = new TRIA[gnTria];
//====== Создание изображения
Sphere(Vert, Trial;
//====== Задание адресов трех массивов (вершин,
//====== нормалей и цветов),
/1====== а также шага перемещения по ним
glVertexPointer(3, GL_FLOAT, sizeof(VERT), &Vert->v); glNormalPointer(GL_FLOAT, sizeof(VERT), &Vert->n);
glColorPointer(3, GL_UNSIGNED_BYTE, sizeof(VERT),
SVert->c);
srand(time(0)); // Подготовка ГСЧ
gShift = CPoint3D (Rand(gMax),Rand(gMax),Rand(gMax));
//====== Формирование списка рисующих команд
glNewListd, GL_COMPILE);
glDrawElements(GL_TRIANGLES, gnTria*3, GL_UNSIGNED_INT, Tria);
glEndList() ;
//== Освобождение памяти, так как список сформирован
delete [] Vert;
delete [] Tria;
}
Формула учета освещенности
Семейство функций glLightModel* позволяет установить общие параметры освещенности сцены. В частности, первый параметр GL_LIGHT_MODEL_AMBIENT сообщает OpenGL, что второй параметр содержит четыре компонента, задающие RGBA-интенсивность освещенности всей сцены. По умолчанию вектор освещенности сцены равен (0.2, 0.2, 0.2, 1.0). Команда glLight* устанавливает параметры источника света. Мы пользуемся ею два раза для задания диффузного и рефлективного компонента интенсивности света. Если вы обратитесь к документации, то увидите, что с помощью glLight* можно задать еще более десятка параметров источника света. Формулу учета освещения я нашел в документации лишь в словесном описании, но рискну привести ее в виде математического выражения.
В режиме RGBA-интенсивность каждого из трех компонентов цвета освещенной вершины вычисляется как сумма нескольких составляющих. Первая составляющая учитывает эмиссию света материалом, вторая — освещенность окружения (ambient) или всей сцены, третья — является суммой вкладов от всех источников света. Максимально допустимое число источников, как вы помните, определено константой GL_MAX_LIGHTS, которая в нашем случае равна 8:
L=Me+MaLaf+Сумма(MaLai+MdLdi(N*Vl)+MsLsi(Ve*Vl)^h)
Здесь символ т обозначает некоторое свойство материала, а символ / — свойство света. Индекс е в применении к материалу обозначает эмиссию, а в применении к
вектору v — eye (глаз). Остальные индексы в применении к материалу обозначают различные компоненты его отражающих свойств.
Md — коэффициент отражения рассеянного (diffuse) отражения,
Ms — коэффициент отражения зеркального (specular) отражения,
N— вектор нормали вершины, который задан командой glNormal,
V1— нормированный вектор, направленный от вершины к источнику света,
Ve — нормированный вектор, направленный от вершины к глазу наблюдателя,
h — блесткость (shininess) материала.
Члены в круглых скобках — это скалярные произведения векторов. Если они дают отрицательные значения, то конвейер заменяет их нулем. Alpha-компонент результирующего цвета освещения устанавливается равным alpha-компоненту диффузного отражения материала. Так как мы задали лишь один источник света (LIGHTO), то знак суммы можно опустить. Обратите внимание на то, что блесткость материала уменьшает (обостряет) пятно отраженного света, так как возведение в степень h > 1 чисел (v, -v,), меньших единицы, уменьшает их значение. Параллельные векторы v, и v, дадут максимальный вклад. Чем больше их рассогласование, тем меньший вклад даст последний член формулы.
Ориентация поверхности
Кроме установки параметров света код функции init содержит довольно много других установок, которые мы осуществляем впервые, поэтому обсудим их более подробно. Возможно, вы помните из курса аналитической геометрии, что некоторые поверхности имеет ориентацию. По умолчанию поверхность любого полигона считается лицевой (FRONT), если вы задали ее обходом вершин против часовой стрелки, и она считается изнаночной (BACK), если направление обхода было обратным. В частности, ориентация поверхности влияет на ориентацию нормали.
Примечание 1
Примечание 1
Вы можете реверсировать эту установку, задав режим glfrontFace (GL_CW). По умолчанию действует установка glFrontFace(GL_CCW). Аббревиатура CW означает clockwise (по часовой стрелке), a CCW — counterclockwise (против часовой стрелки). Кстати, вы, вероятно, видели в литературе изображение ленты Мебиуса или бутылки Клейна, поверхности которых односторонние и поэтому не имеют ориентации.
Команда glEnable (GL_CULL_FACE); включает механизм учета ориентации поверхности полигонов. Она должна сопровождаться одним из флагов, определяющих сторону поверхности, например glCullFace(GL_BACK);. Таким образом, мы сообщаем конвейеру OpenGL, что обратные стороны полигонов можно не учитывать. В этом случае рисование полигонов ускоряется. Мы не собираемся показывать внутреннюю поверхность замкнутой сферы, поэтому эти установки нам вполне подходят.
Массив вершин, нормалей и цветов
Три команды glEnableClientstate говорят о том, что при формировании изображения будут заданы три массива (вершин, нормалей и цветов), а три команды вида gl* Pointer непосредственно задают адреса этих массивов. Здесь важно правильно задать не только адреса трех массивов, но и шаги перемещения по ним. Так как мы вместо трех массивов пользуемся одним массивом структур из трех полей, то шаг перемещения по всем трем компонентам одинаков и равен sizeof (VERT) — размеру одной переменной типа VERT. Массив вершин (vert типа VERT*) и индексов их обхода (Tria типа TRIA*) создается динамически внутри функции init. Характерно, что после того, как закончилось формирование списка рисующих команд OpenGL, мы можем освободить память, занимаемую массивами, так как вся необходимая информация уже хранится в списке. Формирование массивов производится в функции Sphere, которую еще предстоит разработать.
Далее по коду Init идет формирование списка рисующих команд. Так как массивы вершин и индексов их обхода при задании треугольников уже сформированы, то список рисующих команд создается с помощью одной команды glDrawElements. Ее параметры указывают:
размер массива индексов, описывающих порядок выбора вершин (gnTria*3);
тип переменных, из которых составлен массив индексов (GL_UNSIGNED_INT);
адрес начала массива индексов.
Команды:
srandftime(0)); // Подготовка ГСЧ
gShift = CPoint3D(Rand(gMax), Rand(gMax), Rand(gMax));
позволяют задать характер вращения сферы. Константа const double gMax = 5.;
выполняет роль регулятора (ограничителя) степени подвижности сферы. Если вам захочется, чтобы сфера вращалась более резво, то увеличьте эту константу и перекомпилируйте проект.
Формирование массива вершин и индексов
Самой сложной задачей является правильное вычисление координат всех вершин треугольников и формирование массива индексов Tria, с помощью которого команда glDrawElements обходит массив Vert при задании треугольников. Функция Sphere реализует алгоритм последовательного обхода сначала всех сферических треугольников вокруг полюсов сферы, а затем обхода сферических четырехугольников, образованных пересечением параллелей и меридианов. В процессе обхода формируется массив вершин vert. После этого обходы повторяются для того, чтобы заполнить массив индексов Tria. Северный и южный полюса обрабатываются индивидуально. Для осуществления обхода предварительно создаются константы:
Для упрощения восприятия алгоритма следует учитывать следующие особенности, связанные с порядком обхода вершин:
Координаты вершин (х, z) представляют собой проекции точек на экваториальную плоскость, а координата у постоянна для каждой широты.
При обработке одной секции кольца для двух треугольников формируется по три индекса:
void Sphere(VERT *v, TRIA* t)
{
//====== Формирование массива вершин
//====== Северный полюс
v[0].v = CPointSD (0, gRad, 0);
v[0].n = CPoint3D (0, 1, 0);
v[0].с = gClr2;
//====== Индекс последней вершины (на южном полюсе)
UINT last = gnVert - 1; //====== Южный полюс
v[last].v = CPointSD (0, -gRad, 0);
v[last].n = CPointSD (0, -1, 0) ;
v[last].c = gnVert & 1 ? gClr2 : gClrl;
//====== Подготовка констант
double da = PI / (gnRings +2.),
db = 2. * PI / gnSects,
af = PI - da/2.;
bf = 2. * PI - db/2.;
//=== Индекс вершины, следующей за северным полюсом
UINT n = 1;
//=== Цикл по широтам
for (double a = da; a < af; a += da)
{
//=== Координата у постоянна для всего кольца
double у = gRad * cos(a),
//====== Вспомогательная точка
xz = gRad * sin(a);
//====== Цикл по секциям (долгота)
for (double b = 0.; b < bf; n++, b += db)
}
// Координаты проекции в экваториальной плоскости
double х = xz * sin(b), z = xz * cos(b);
//====== Вершина, нормаль и цвет
v[n].v = CPointSD (x, у, z);
v[n].n = CPointSD (x / gRad, у / gRad, z / gRad);
v[n].c = n & 1 ? gClrl : gClr2; } }
//====== Формирование массива индексов
//====== Треугольники вблизи полюсов
for (n = 0; n < gnSects; n++)
{
//====== Индекс общей вершины (северный полюс)
t[n] .11 = 0;
//====== Индекс текущей вершины
t[n] .12 = n + 1;
//====== Замыкание
t[n].13 = n == gnSects - 1 ? 1 : n + 2;
//====== Индекс общей вершины (южный полюс)
t [gnTria-gnSects+n] .11 = gnVert - 1;
t tgnTria-gnSects+n] . 12 = gnVert - 2 - n;
t [gnTria-gnSects+n] .13 = gnVert - 2
t ( (1 + n) % gnSects) ;
}
//====== Треугольники разбиения колец
//====== Вершина, следующая за полюсом
int k = 1;
//====== gnSects - номер следующего треугольника
S' n = gnSects;
for (UINT i = 0; i < gnRings; i++, k += gnSects) {
for (UINT j = 0; j < gnSects; j++, n += 2) {
//======= Индекс общей вершины
t[n] .11 = k + j;
//======= Индекс текущей вершины
t[n].12 = k + gnSects + j;
//======= Замыкание
t[n].13 = k + gnSects + ((j + 1) % gnSects)
//======= To же для второго треугольника
t[n + 1].11 = t[n].11;
t[n + 1].12 = t[n].13;
t[n + 1].13 = k + ((j + 1) % gnSects);
Для завершения работы осталось дополнить программу стандартным набором процедур, алгоритм функционирования которых вы уже изучили:
void_stdcall OnSize(GLsizei w, GLsizei h) { glViewport(0, 0, w, h);
}
void main ()
{
auxInitDisplayMode(AUX_RGB | AUX_DOUBLE) ;
auxInitPositiondO, 10, 512, 512);
auxInitwindow("Vertex Array");
Init() ;
auxReshapeFunc (OnSize) ;
auxIdleFunc (OnDraw) ;
auxMainLoop (OnDraw) ;
}
Запустите проект на выполнение и уберите возможные неполадки. Исследуйте функционирование программы, вводя различные значения глобальных параметров (регулировок). Попробуйте задать нечетное число секций. Объясните результат. В качестве упражнения введите возможность интерактивного управления степенью дискретизации сферы и исследуйте эффективность работы конвейера при ее увеличении.
Рекурсивное деление
Рекурсивное деление
Добавим возможность дальнейшего деления треугольников и образования фигур, приближающихся к сфере. Осуществление предельного перехода возможно в математике непрерывных величин, но невозможно в дискретной математике и тем более в реализациях ее алгоритмов, где все множества должны быть конечными. Однако мы можем применить конечную рекурсию для дальнейшего деления треугольников. Замените функцию split на ее рекурсивную версию. Вы, конечно, помните, что рекурсивная функция вызывает сама себя до тех пор, пока не выполнится некоторое условие. Здесь условием выхода из цепи рекурсивных вызовов является равенство нулю последнего параметра depth, который определяет текущую глубину рекурсии:
void Split(double *vl, double *v2, double *v3,long depth)
{
double v12[3], v23[3], v31[3];
if (depth == 0)
{
//======
Рисование наименьших треугольников
Рисование наименьших треугольников
setTria(vl, v2, v3);
//====== и выход из цепи рекурсивных вызовов
return;
}
//====== Разбиение треугольника
for (int i = 0; i < 3; i++)
{
v12[i] = vl[i]+v2[i];
v23[i] = v2[i]+v3[ij;
v31[i] = v3[i]+vl[i];
}
//====== Дотягивание до сферы
Scale(v12);
Scale(v23);
Scale(v31); //====== Рекурсивное разбиение на
//====== четыре внутренних треугольника
Split(vl, v!2, v31, depth-1);
Split(v2, v23, v12, depth-1);
Split(v3, v31, v23, depth-1);
Split(v!2, v23, v31, depth-1);
}
Внесите также изменение в ту строку программы, где происходит вызов Split. Надо добавить параметр, задающий глубину рекурсии. Если функцию вызвать с нулевой глубиной, то получим икосаэдр, если увеличивать глубину, то будем получать фигуры, более близкие к шару:
for (int i = 0; i < 20; i++)
Split (v[id[i) [0]], v[id[i][l]], v[id[i] [2]], 3);
Запустите и проверьте, нажимая клавишу N. Попробуйте изменить глубину рекурсии, только не переусердствуйте. Если задать глубину более 10, то можно не дождаться ответа. Рекурсия дорого стоит, поэтому исследованный подход абсолютно неприемлем для создания сферы. Аналогичный вывод справедлив для других объемных изображений, создаваемых с помощью задания вершин большого числа геометрических примитивов.
В данный момент для иллюстрации процесса приближения изображаемой фигуры к сфере напрашивается такой сценарий: пользователь нажимает клавишу — пробел, глубина рекурсии изменяется и изображение пересчитывается. Алгоритм управления глубиной рекурсии, очевидно, следует выбрать таким, чтобы, оставаясь в рамках допустимых значений, можно было проходить весь диапазон в обе стороны. Введите в функцию main обработку нажатия клавиши пробела:
auxKeyFunc(AOX_SPACE, KeySpace);
и создайте функцию обработки:
void _stdcall KeySpace()
{
//====== Флаг роста числа разбиений
static bool bGrow = true;
//====== Продолжаем разбивать до глубины 4
if (bGrow SS giDepth < 4)
{
giDepth += 1;
}
//====== Смена знака при глубине 4
else if (giDepth > 0)
{
bGrow = false;
giDepth == 1;
}
//====== Смена знака при глубине О
else
{
bGrow = true;
giDepth += 1;
}
DrawScene () ;
}
Алгоритм предполагает, что глобально определена переменная giDepth, которая хранит текущее значение глубины рекурсии. Добавьте к существующим глобальным переменным объявление:
//====== Глубина рекурсии
int giDepth;
В функции DrawScene замените параметр 3 (при вызове Split) на giDepth и запустите на выполнение.
Примечание 1
Примечание 1
Не знаю, как объяснить, но в Visual Studio б этот код почему-то работает, не-— смотря на явный промах, который типичен не только для начинающих программистов. Опытный читатель, конечно же, заметил, что мы создаем новые списки изображений, не уничтожая старые. Такие действия классифицируются как утечка памяти (memory lickage). Для ее устранения вставьте следующий фрагмент в функцию DrawScene перед вызовом glNewList:
//====== Если существует 1-й список,
if (gllsList(1))
//====== то освобождаем память
glDeleteLists (1,1);
Разъяснения можно найти в справке по функциям gllsList и glDeleteLists. He ошибитесь при выборе места вставки фрагмента, так как операции с памятью имеют особую важность. Запустите приложение и, нажимая на пробел, наблюдайте за изменением изображения, которое сначала приближается к сфере, затем постепенно возвращает свой первоначальный облик икосаэдра. Периодически нажимайте клавишу N для того, чтобы оценить влияние точного вычисления нормалей.
Рисуем
Рисуем
glColorSf (O.,0.,0.); // Меняем цвет на черный
glPolygonStipple (gSpade);
// Меняем узор glRectf (220., 20., 315., 120.);
glPolygonStipple (gStrip); // Меняем узор
glColor3f (0., 0.6f, 0.3f);
glRectf (320., 20., 415., 120.);
//== Готовимся заполнить более сложный, невыпуклый
//== (nоn convex) полигон
glPolygonStipple (gSpade);
glColorSd (0.6, O.f, 0.3f);
//======= Шесть вершин по три координаты
float c[6][3] =
{
420.,120.,0.,
420.,70.,0.,
470.,20.,0., 520., 70.,0.,
520.,120.,0.,
470.,100.,0.
};
//== Здесь мы специально выбираем nоn convex полигон,
//== чтобы увидеть как плохо с ним обходится OpenGL
glBegin (GL_POLYGON) ;
for (int i=0; i<6; i++)
glVertex3fv(c[i] ) ;
glEnd() ;
glDisable (GL_POLYGON_STIPPLE) ;
glFlush ();
}
Запустите и убедитесь в том, что последний полигон потерял одну точку. Затем замените цикл задания его вершин на:
for (int i=5; i>=0; i--) glVertex3fv(c[i]) ;
Здесь мы изменили порядок обхода вершин и начали с вогнутой вершины. Запустите и убедитесь в том, что теперь в полигоне есть все шесть вершин. OpenGL не гарантирует точную передачу вогнутых полигонов. Поэтому для надежной передачи их надо предварительно разбивать на выпуклые части. Если этими частями будут треугольники, то процесс разбиения называется tessellation (мозаика). Есть специальные функции для тесселяции полигонов. Их рассмотрение выходит за рамки этой книги. Попробуйте самостоятельно задать рассмотренный выше полигон в виде двух выпуклых четырехугольников. Для этого посмотрите справку по функции glBegin с параметром GL_QUADS.
Полигоны можно рисовать либо закрашенными (режим — GL_FILL), либо в скелетном виде (GL_LINE), либо в виде намеков (GL_POINT). Испробуйте все режимы на примере невыпуклой звезды. При рисовании точками попробуйте предварительно дать команду glPointSize (5):
void _stdcall OnDraw()
{
glClear (GL_COLOR_BUFFER_BIT);
glColor3d (1., 0.4, 1.);
//=== 2 угла, характеризующие звезду и
//=== 2 характерные точки
double pi = 4. * atan(l.),
al = pi / 10., a2 = 3. * al,
xl = costal), yl = sin(al)',
x2 = cos(a2), y2 = sin(a2);
//=== Мировые координаты вершин нормированной звезды
double с[5][3] =
{
0., 1., 0.,
-х2, -у2, 0.,
xl, yl, 0.,
-xl, yl, 0.,
х2, -у2, 0.,
};
//====== Оконные координаты
for (int i=0; i<5; i+t)
{
c[i][0] = 250 + 100*c[i][0];
c[i][l] = 100 + 100*c[i] [1] ;
}
//=== Режим заполнения полигона - скелетный
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
//=== Задаем вершины полигона
glBegin(GL_POLYGON);
for (i=0; i<5; i++)
glVertex3dv(c[i] ) ;
glEnd() ;
glFlush() ;
}
Рисуем сначала unstippled rectangle (без узора)
Рисуем сначала unstippled rectangle (без узора)
//== Rect - это тоже полигон
glRectf (20., 20., 115., 120.);
glColor3f (1., 0., 0.); // Меняем цвет на красный
glEnable (GL_POLYGON_STIPPLE); // Включаем штриховку
glPolygonStipple (gStrip); // Задаем узор
glRectf (120., 20., 215., 120.); //
Схема конвейера OpenGL
Рисунок 6.1. Схема конвейера OpenGL
Списки команд OpenGL (Display Lists)
Все данные, описывающие геометрию или отдельные пикселы, могут быть сохранены в списках команд (display lists) для последующего использования. Альтернатива — немедленное использование (immediate mode). При вызове списка командой glCallList сохраненные данные из списка начинают двигаться по конвейеру так же, как и в режиме немедленного использования.
Вычислители (Evaluators)
Все геометрические примитивы описываются своими вершинами. Параметрические кривые и поверхности могут изначально-быть описаны контрольными точками или базовыми функциями (обычно полиномиальными). Вычислители — это методы, которые генерируют координаты вершин, нормали к поверхности, координаты текстур и цвета точек, опираясь на контрольные точки.
Сборка примитивов
На этом этапе происходит преобразование вершин в примитивы. Пространственные координаты (х, у, z) преобразовываются с помощью матриц размерностью (4 х 4). Основная цель — получить экранные, двухмерные координаты из трехмерных, мировых координат. Если включен режим генерации текстуры, то она создается на этом этапе. Освещенность вычисляется исходя из координат вектора нормали, расположения источников света, отражающих свойств материала, углов конусов света и параметров его аттенюации (ослабления). В результате получается цвет пиксела. Важным моментом на этапе сборки примитивов (primitive assembly) является отсечение (clipping), то есть удаление тех частей геометрии, которые попадают в невидимую часть пространства. Точечное отсечение пропускает или не пропускает вершину. Отсечение линий или полигонов подразумевает не только удаление вершин, но и возможное добавление некоторых (промежуточных) вершин. На этом этапе происходит учет перспективы, то есть уменьшение тех деталей сцены, которые расположены дальше от точки наблюдения, и увеличение тех деталей, которые расположены ближе. Здесь используется понятие видимого объема (viewport). Режим заполнения промежуточных точек полигона тоже играет роль на этапе сборки.
Операции с пикселами (Pixel Operations)
Данные о пикселах следуют в конвейере OpenGL параллельным путем. Данные, хранимые в массивах системной памяти, распаковываются с учетом набора возможных форматов, затем масштабируются, сдвигаются и обрабатываются так называемой картой пикселов (pixel map). Результат записывается либо в память текстуры, либо посылается на следующий этап — растеризацию. Отметьте, что возможна обратная операция считывания пикселов. При этом также Выполняются операции: масштабирование, сдвиг, преобразование и упаковка и помещение в системную память. Существуют специальные операции копирования данных из буфера кадра (framebuffer) в другую его часть или в буфер текстуры.
Сборка текстуры (Texture Assembly)
Текстуры — это bitmap-изображения, накладываемые на поверхности геометрических объектов для придания эффекта фактуры реального материала. Текстурные объекты создаются в OpenGL для упрощения их повторного использования. Использование текстур сопряжено с большими затратами, поэтому в работе с ними применяют специальные ресурсы, такие как texture memory. Так называют быструю видеопамять, приоритет использования которой отдается текстурным объектам.
Растеризация
Так называют преобразование как геометрических, так и данных о пикселах во фрагменты. Каждый фрагмент соответствует пикселу в буфере кадра. При вычислении цвета фрагмента учитывается большое количество факторов: узор штриховки полигона или линии, толщина линии и размер точки, сглаживание зубчатости линий, тень объекта, режим заполнения полигона, учет глубины изображения (факт видимости или невидимости) и др.
Операции с фрагментами
Каждая точка уже двухмерного изображения характеризуется цветом, глубиной (значением координаты Z) и данными о текстуре. Такая точка вместе с сопутствующей информацией называется фрагментом. Фрагмент изменяет соответствующий ему пиксел в буфере кадра, если он проходит пять тестов:
Scissor-тест, который проверяет принадлежность вырезаемому прямоугольнику, который задается функцией glScissor;
Alpha-тест, который проверяет четвертый компонент цвета — прозрачность фрагмента с помощью функции glAlphaFunc;
Stencil-тест, используемый при создании специальных эффектов. Он, например, проверяет, не попал ли фрагмент в промежуток регулярного узора;
Depth-buffer-тест, который проверяет, не закрыт ли фрагмент другим фрагментом с меньшей координатой Z.
Кроме того, фрагмент претерпевает другие изменения.
вычисление дымки (fog);
смешивание (blending);
интерполяция цвета (dithering);
логические операции;
маскирование с помощью трафарета (bitmask).
Ориентация вектора нормали
Рисунок 6.2. Ориентация вектора нормали
Если координаты векторов а и b известны, то координаты нормали вычисляю по следующим формулам. Длина вектора нормали п зависит от длин вектор сомножителей и величины угла между ними:
Nx=AxBz-AzBy
Ny=AzBx-AxBz
Nz=AxBy-AyBx
Примечание 1
Примечание 1
Можно потерять много времени на осознание того факта, что не только правление нормали, но и ее модуль влияют на величину освещенности (и та) вершины, так как сопровождающая документация (Help) не содер; явных указаний на это. Отметьте также, что цвета вершин полигона влияю цвета точек заполнения полигона, так как цвета вновь генерируемых то интерполируются, то есть принимают промежуточные значения между з чениями цвета вершин.
Чтобы нивелировать зависимость цвета вершины от амплитуды нормали, обыч вектор нормали масштабируют (или нормируют), то есть делают его длину р; ной единице, оставляя неизменным направление. С учетом сказанного создал две вспомогательные функции. Первая масштабирует, а вторая вычисляет н< маль к плоскости треугольника. Алгоритм вычисления использует координа двух сторон, прилегающих к текущей вершине треугольника:
//====Нормирование вектора нормали (или любого другого)
void Scale(double v[3])
{
double d = sqrt(v[0]*v[0]+v[l]*v[l]+v[2]*v[2]);
if (d == 0.)
{
MessageBox(0,"Zero length vector","Error",MB_OK);
return;
}
void getNorm(double vl[3], double v2[3], double out[3])
{
//===== Вычисляем координаты вектора нормали
//====== по формулам векторного произведения
out[0] = vl[l]*v2[2] - vl[2]*v2[l];
out[l] = vl[2]*v2(0] - vl[0]*v2[2] ;
out[2] =vl[0]*v2[l] - vl[l]*v2[0];
Scale(out);
}
Замените функцию DrawScene. В новом варианте мы аккуратно вычисляем и масштабируем нормали в каждом из двадцати треугольников поверхности икосаэдра:
void DrawScene()
{
static double
angle - 3. * atanfl.)/2.5, V = cos(angle), W = sin(angle),
v[12] [3] = {
{-V,0.,W}, {V,0.,W}, {-V,0.,-W},
{V,0.,-W}, {0.,W,V}, {0.,W,-V},
{0.,-W,V}, {0. ,-W,-V}, {W,V, 0.},
{-W,V,0.}, {W,-V,0.}, {-W,-V,0.}
};
static GLuint id[20][3] = {
(0,1, 4), {0,4, 9}, (9,4, 5), (4,8, 5}, (4,1,8),
(8,1,10), (8,10,3), (5,8, 3), (5,3, 2), (2,3,7),
(7,3,10), (7,10,6), (7,6,11), (11,6,0), (0,6,1),
(6,10,1), (9,11,0), (9,2,11), (9,5, 2), (7,11,2) 1;
glNewList(l,GL_COMPILE); glColorSd (1., 0.4, 1.) ;
glBegin(GLJTRIANGLES);
for (int i = 0; i < 20; i++)
{
double dl[3], d2[3], norm[3];
for (int j = 0; j < 3; j++)
{
dl[j] =v[id[i][0]] [j] -v[id[i][l]J [j];
d2[j] =v[id[i][l]] [j] -v[id[i][2J] [j];
}
//====== Вычисление и масштабирование нормали
getNorm(dl, d2, norm);
glNormal3dv(norm);
glVertexSdv(v [ id[i] [1]]);
glVertex3dv(v[id[i] [1] ] glVertex3dv(v[id[i] [2] ]
glEnd() ;
}
glEndList () ;
}
Примечание 2
Примечание 2
Функцию нормировки всех нормалей можно возложить на автомат OpenGL, если включить состояние GL_NORMALIZE, но обычно это ведет к замедлению перерисовки и, как следствие, выполнения приложения, если изображение достаточно сложное. В нашем случае оно просто, и поэтому вы можете проверить действие настройки, если вставите вызов glEnable (GL_NORMALIZE); в функцию Init (до вызова OrawScene) и временно выключите вызов Scale(out); производимый в функции getNorm. Затем вернитесь к исходному состоянию.
Деление треугольника икосаэдра
Рисунок 6.3. Деление треугольника икосаэдра
Треугольник с вершинами VI, V2 и V3 разбивается на четыре треугольника: (V1,V12,V31), (V2,V23,V12), (V3,V32,V23) и (V12.V23.V31). После этого промежуточные точки деления надо посадить на поверхность шара, то есть изменить их координаты так, чтобы концы векторов (V12, V23 и V31) дотянулись до поверхности шара. Для этого достаточно нормировать векторы с помощью уже существующей процедуры Scale. Она впоследствии будет использована как для масштабирования нормали, так и для нормировки координат вершин новых треугольников. Но сейчас мы будем вычислять нормаль приближенно. Введем еще две вспомогательные функции:
//=== Команды OpenGL для изображения одного треугольника
void setTria(double *vl, double *v2, double *v3)
{
//====== Нормаль и вершина задаются одним вектором
glNormal3dv(vl);
glVertex3dv(vl);
glNormalSdv (v2);
glVertex3dv(v2);
glNormal3dv(v3);
glVertex3dv(v3);
glEnd() ;
}
//====== Генерация внутренних треугольников
void Split(double *vl, double *v2, double *v3)
{
//====== Промежуточные вершины
double v!2[3], v23[3], v31[3);
for (int l=0; l< 3; i++) {
//====== Можно не делить пополам,
//====== так как будем нормировать
v12[i] = vl[i]+v2[i];
v23[i] = v2[i]+v3[i];
v31 [i] = v3[i]+vl [i];
}
//====== Нормируем три новые вершины
Scale(v!2);
Scale(v23);
Scale(v31); //====== и рисуем четыре треугольника
setTria(vl, v!2, v31);
setTria (v2, v23, v!2);
setTria(v3, v31, v23);
setTria(v!2,v23, v31);
}
Вставьте эти глобальные функции в файл и дайте следующую версию функцию DrawScene, в которой отсутствует вызов функции getNorm для точного вычисления нормали, но есть вызов функции Split для каждой из 20 граней икосаэдра. В результате мы получаем фигуру из 80 треугольных граней, которая значительно ближе к сфере, чем икосаэдр:
void DrawScene()
{
static double
angle = 3. * atan(l.)/2.5, V = cos (angle), W = sin (angle),
v[12] [3] =
{-V,0.,W}, {V,0.,W}, {-V,.0.,-W},
(V,0.,-W), {0.,W,V}, {0.,W,-V},
(0.,-W,V), (0.,-W,-V), {W,V,0.},
{-W,V,0.}, {W,-V,0.}, {-W,-V,0.}
};
static GLuint id[20][3] =
{
(0,1, 4), (0,4, 9), {9,4, 5}, (4,8, 5), (4,1,8),
(8,1,10), (8,10,3), (5,8, 3), (5,3, 2), (2,3,7),
(7,3,10), (7,10,6), (7,6,11), (11,6,0), (0,6,1),
(6,10,1), (9,11,0), (9,2,11), (9,5, 2), (7,11,2)
};
glNewList(l,GL_COMPILE);
glColor3d (1., 0.4, 1.) ;
glBegin(GLJTRIANGLES);
for (int i = 0; i < 20; i++)
Split (v[id[i][0]], v[id[i][l]], v[id[i] [2] ]) ;
glEnd() ;
glEndList () ;
}
На этой стадии я рекомендую посмотреть, какие интересные и неожиданные результаты могут быть получены вследствие ошибок. Все мы ошибаемся, вот и я так долго возился с направлением обхода и со знаком нормали, что в промежуточных вариантах получал чудовищные комбинации. Многие из них «канули в Лету», но один любопытный вариант легко смоделировать. Если ошибки происходят в условиях симметричного отражения, то возникают ситуации, сходные со случайными изменениями узоров в калейдоскопе. Замените на обратные знаки компонентов вектора в функции Scale. Это действие в предыдущих версиях программы было эквивалентно изменению знака нормали. Найдите строку, похожую на ту, что приведена ниже, и замените знаки так, как показано, на минусы.
v[0] /= -d; v[l] /= -d; v[2] /= -d;
Штриховка линий
Штриховка линий
Основные действия разворачиваются в функции перерисовки. Здесь мы рисуем несколько линий, изменяя узор их штриховки. Режим штриховки линий включается командой glEnable (GL_LINE_STIPPLE). Узор штриховки задается параметрами функции glLineStipple. Первый параметр является коэффициентом повторения, а второй определяет сам узор. Он должен быть 16-битовой константой или переменной, последовательность бит которой определяет последовательность фрагментов при растеризации линии. Порядок использования битов возрастающий, то есть нулевой бит используется первым. Каждому пикселу соответствует один бит, что характеризует ситуацию: текущий цвет либо включен (бит равен 1), либо выключен (бит равен 0). Алгоритм станет очевидным, если вы запустите приложение (Ctrl+F5), устраните возможные ошибки и увидите результат. Обратите внимание на различие штриховки в 3-й и 4-й строках. Попытайтесь объяснить различие. Создайте несколько своих собственных узоров штриховых (stippled) линий.
Штриховка полигонов
Штриховка полигонов
Теперь применим штриховку (stipple) к полигонам. Режим штриховки включается и выключается стандартным способом:
glEnable (GL_POLYGON_STIPPLE) ;
glDisable (GL_POLYGON_STIPPLE);
Bitmap-узор (pattern) штриховки надо предварительно подготовить в массиве такой размерности, чтобы заполнить bitmap площадью 32x32 = 1024 пиксела. Размерность массива с узором определяется так: 1024 бита можно разместить в 128 переменных по одному байту. Мы разместим их в 16 строках по 8 байт. Имеем 16 х х 8 х 8 = 1024 бита (или пиксела).
Массивы объявлены глобально. Адреса массивов с узором подаются на вход функции glPolygonStipple:
GLubyte gSpade[] = // Узор - пики
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xlf, 0xff, Oxff, 0xf8, 0xlf, 0x00, 0x00, 0xf8,
0x01, OxcO, 0x03, 0x80, 0x00, 0x70, 0xOe, 0x00,
0x00, |
0x20, |
0x04, |
0x00, |
0x00, |
0x30 |
, 0x0c, |
0x00, |
||
0x00, |
0x10, |
0x08, |
0x00, |
0x00, |
0x18 |
, 0x18, |
0x00, |
||
0x07, |
0хс4, |
0x23, |
0xe0, |
0x0f, |
0xf8 |
, 0xlf, |
0xf0, |
||
0x38, |
0xlc, |
0x38, |
0xlc, |
0x30, |
0x00 |
, 0x00, |
0x0c, |
||
0x60, |
0x00, |
0x00, |
0x06, |
0x60, |
0x00 |
, 0x00, |
0x06, |
||
0x60, |
0x00, |
0x00, |
0x06, |
0x60, |
0x00 |
, 0x00, |
0x06, |
||
0x60, |
0x00, |
0x00, |
0x06, |
0x30, |
0x00 |
, 0x00, |
0x0c, |
||
0x30, |
0x00, |
0x00, |
0x0c, |
0x18, |
0x00 |
, 0x00, |
0x18, |
||
0х0е, |
0x00, |
0x00, |
0x70, |
0x03, |
0x00 |
, 0x00, |
0xc0, |
||
0x00, |
OxcO, |
0x03, |
0x00, |
0x00, |
0x70 |
, 0x0e, |
0x00, |
||
0x00, |
0x18, |
0x18, |
0x00, |
0x00, |
0x0c |
, 0x30, |
0x00, |
||
0x00, |
0x07, |
OxeO, |
0x00, |
0x00, |
0x03 |
, 0xc0, |
0x00, |
||
0x00, |
0x01, |
0x80, |
0x00, |
0x00, |
0x00 |
, 0x00, |
0x00 |
||
GLubyte |
gStripU = |
// Другой узор - |
полоса |
|
|||||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
Функцию OnDraw замените целиком. Так же поступайте и дальше. Следите лишь за изменениями в функциях main, OnSize и init, которые понадобятся при радикальной смене режима передачи (rendering). Позже мы перейдем к передаче трехмерной графики, а пока режим тот же — двухмерная графика:
void _stdcall OnDraw()
{
//====== Стираем окно
glClear (GL_COLOR_BUFFER_BIT);
//====== Цвет фона (синеватый)
glColor3f (0.3f, 0.3f, 1.);
//==
Создание консольного проекта
Создание консольного проекта
Для исследования возможностей функций библиотек OpenGL целесообразно создать простой проект консольного типа, в котором для работы с другим (Windows) окном будут использованы функции дополнительной библиотеки OpenGL, описанной в файле GLAUX.LIB. Рассмотрим последовательность шагов для создания нового проекта консольного типа.
Задайте имя проекта Console, желаемое местоположение папки с проектом и нажмите ОК.
Поставьте фокус на элемент Console в окне Solution Explorer, вызовите контекстное меню и выберите команду Add > Add New Item.
В окне диалога Add New Item перейдите в список Templates и выберите строку C++File(.cpp).
В поле Name того же диалога задайте имя файла OG.cpp и нажмите кнопку Open.
Далее вы будете вводить код в окно редактора Studio.Net (вкладка OG.cpp). Для того чтобы компоновщик подключил все библиотеки OpenGL, произведите настройку проекта.
В окне открывшегося диалога Console Property Pages выберите элемент дерева Linker * Input.
Переведите фокус в поле Additional Inputs окна справа и добавьте в конец существующего текста имена файлов с описаниями трех библиотек: OPENGL32.LIB GLU32.LIB GLAUX.LIB. Убедитесь в том, что все имена разделены пробелами и нажмите ОК.
В новый пустой файл OG.cpp поместите следующий код приложения, которое для создания Windows-окна пользуется услугами библиотеки GLAUX.LIB. Для этого необходимо к проекту консольного типа подключить файл Windows.h1:
#include <Windows.h>
#include <math.h>
//====== Подключение заголовков библиотек OpenGL
#include <GL\gl.h>
# include <GL\glu.h>
#include <GL\Glaux.h>
//=====Макроподстановка для изображения одной линии
#define Line(xl,yl,x2,y2) \
glBegin(GL_LINES); \
glVertex2d ( (xl), (yl)); \
glVertex2d ((x2),(y2)); \
glEnd() ;
//====== Реакция на сообщение WM_PAINT
void _stdcall OnDraw()
{
//====== Стираем буфер кадра (framebuffer)
glClear (GL_COLOR__BUFFER_BIT) ;
//====== Выбираем черный цвет рисования
glColorSf (0., 0., 0.);
//=== В 1-м ряду рисуем 3 линии с разной штриховкой
glEnable (GL_LINE_STIPPLE);
glLineWidth (2.);
glLineStipple (1, 0x0101); // dot
Line (50., 125., 150., 125.);
glLineStipple (1, OxOOFF); // dash
Line (150., 125., 250., 125.);
glLineStipple (1, OxlC47); // dash/dot/dash
Line (250., 125., 350., 125.);
//====== Во 2-м ряду то же, но шире в 6 раз
glLineWidth (6.);
glLineStipple (1, 0x0101); // dot
Line (50., 100., 150., 100.);
glLineStipple (1, OxOOFF); // dash
Line (150., 100., 250., 100.);
glLineStipple (1, OxlC47); // dash/dot/dash
Line (250., 100., 350., 100.);
//== Во 3-м ряду 7 линий являются частями
//== полосы (strip). Учетверенный узор не прерывается
glLineWidth (2.);
glLineStipple (4, OxlC47); // dash/dot/dash
glBegin (GL_LINE_STRIP);
for (int i =1; i < 8; i++)
glVertex2d (50.*i, 75.); glEnd() ;
//== Во 4-м ряду 6 независимых, отдельных линий
//== Тот же узор, но он каждый раз начинается заново
for (i = 1; i < 7; i++)
{
Line (50*1, 50, 50* (i+1), 50);
}
//====== во 5-м ряду 1 линия с тем же узором
glLineStipple (4, OxlC47); // dash/dot/dash
Line (50., 25., 350., 25.);
glDisable (GL_LINE_STIPPLE); glFlush ();
}
//===== Реакция на WM_SIZE
void _stdcall OnSize (int w, int h)
{
glViewport (0, 0, (GLsizei) w, (GLsizei) h);
glMatrixMode (GL_PROJECTION); glLoadldentity();
//====== Режим ортографической проекции
gluOrtho2D (0.0, double(w), 0.0, double(h));
}
//====== Настройки
void Init()
{
//====== Цвет фона - белый
glClearColor (1., 1., 1., 0.);
//====== Нет интерполяции цвета при растеризации
glShadeModel (GL_FLAT); }
void main()
{
//=== Установка pixel-формата и подготовка окна OpenGL
auxInitDisplayMode (AUX_SINGLE | AUX_RGB);
auxInitPosition (200, 200, 550, 250);
auxInitWindow("My Stipple Test");
Init() ;
auxReshapeFunc (OnSize);
// Кого вызывать при WM_SIZE auxMainLoop(OnDraw);
// Кого вызывать при WM_PAINT
}
Функция main содержит стандартную последовательность действий, которые производятся во всех консольных приложениях OpenGL. С ней надо работать как с шаблоном приложений рассматриваемого типа. Первые три строчки функции main устанавливают pixel-формат окна OpenGL. Заботу о его выборе взяла на себя функция auxInitDisplayMode из вспомогательной библиотеки. В параметре мы указали режим использования только одного (front) буфера (бит AUX_SINGLE) и цветовую схему без использования палитры (бит AUX_RGB).
В функции init обычно производят индивидуальные настройки конечного автомата OpenGL. Здесь мы установили белый цвет в качестве цвета стирания или фона окна и режим заполнения внутренних точек полигонов. Константа GL_FLAT соответствует отсутствию интерполяции цветов. Вызов функции auxReshapeFunc выполняет ту же роль, что и макрос ON_WM_SIZE в MFC-приложении. Происходит связывание сообщения Windows с функцией его обработки. Все функции обработки должны иметь тип void _stdcall. Вы можете встретить и эквивалентное описание этого типа (void CALLBACK). Имена функций OnDraw и OnSize выбраны намеренно, чтобы напомнить о Windows и MFC. В общем случае они могут быть произвольными. Важно запомнить, что последним в функции main должен быть вызов auxMainLoop.
В OnSize производится вызов функции glviewport, которая задает так называемый прямоугольник просмотра. Мы задали его равным всей клиентской области окна. Конвейер OpenGL использует эти установки так, чтобы поместить изображение в центр окна и растянуть или сжать его пропорционально размерам окна. Аффинные преобразования координат производятся по формулам:
Xw=(X+1)(width/2)+X0
Yw=(Y+1)(height/2)+Y0
В левой части равенств стоят оконные координаты:
(Хо, Yo) — это координаты левого верхнего угла окна. Они задаются первым и вторым параметрами функции glviewport;
сомножители в формуле (width и height) соответствуют третьему и четвертому параметрам (w, h) функции glviewport и равны текущим значениям размеров окна.
Как видно из подстановки в формулу, точка с координатами (0,0) попадет в центр окна, а при увеличении ширины или высоты окна (width или height) координаты изображения будут увеличиваться пропорционально. Вызов
glMatrixMode (GL_PROJECTION);
определяет в качестве текущей матрицу проецирования, а вызов glLoadldentity делает ее равной единичной матрице. Следующий за этим вызов
gluOrtho2D (0.0, double(w), 0.0, double(h));
задает в качестве матрицы преобразования матрицу двухмерной ортографической (или параллельной) проекции. Изображение будет отсекаться конвейером OpenGL, если его детали вылезают из границ, заданных параметрами функции gluOrtho2D.
Создание сферы
Создание сферы
Для иллюстрации рассматриваемых возможностей мы создадим сферу, составленную из треугольников, но при этом не будем отталкиваться от какого-либо правильного многогранника, а используем модель глобуса. Количество и пропорции треугольников будут зависеть от количества геодезических линий на сфере (параллелей и меридианов). Если вы посмотрите на Рисунок 6.4 или представите себе глобус, то согласитесь с тем, что параллели и меридианы разбивают поверхность сферы на множество сферических четырехугольников. Исключение составляют лишь полюса, вокруг которых мы имеем сферические треугольники. Если затем каждый сферический четырехугольник разделить диагональю, то он даст два сферических треугольника.
Строим икосаэдр
Строим икосаэдр
Для иллюстрации работы с массивами вершин создадим более сложный объект — икосаэдр. Это такой дссятистенный дом с острой пятиугольной крышей и таким же полом, но углы пола смещены (повернуты) на л/5 относительно углов потолка.
Икосаэдр имеет 20 треугольных граней и 12 вершин (1 + 5 на потолке и 1 + 5 на полу). Благодаря своей правильности он может быть задан с помощью всего лишь двух чисел, которые лучше вычислить один раз и запомнить. Этими числами является косинус и синус угла в три пятых окружности, то есть
static double
//====== atan(l.) - это пи/4
angle = 3. * atan(1.)/2.5, //====== 2 характерные точки
V = cos(angle), W = sin(angle);
Этот код мы вставим внутрь функции рисования, чтобы не плодить глобальные переменные и не нарываться на конфликты имен. Вот новая версия функции DrawScene:
void DrawScene() { static double
//====== 2 характерные точки
angle = 3. * atan(l.)/2.5, V = cos(angle), W = sin(angle),
//=== 20 граней икосаэдра, заданные индексами вершин
static GLuint id[20][3] =
(0,1, 4), (8,1,10), (7,3,10), (6,10,1), |
(0,4, 9), (8,10,3), (7,10,6), (9,11,0), |
(9,4, 5), (5,8, 3), (7,6,11), (9,2,11), |
(4,8, 5), (5,3, 2), (11,6,0), (9,5, 2), |
(4,1,8), (2,3,7), (0,6,1), (7,11,2) |
||
//====== Начинаем формировать список команд
glNewList (1,GL_COMPILE) ;
//====== Выбираем текущий цвет рисования
glColor3d (1., 0.4, 1 . ) ;
glBegin (GLJTRIANGLES) ;
for (int i = 0; i < 20; i++)
{
//====== Грубый подход к вычислению нормалей
glNorma!3dv(v[id[i] [0] ] ) ;
glVertex3dv(v[id[i] [0] ] ) ;
glNorma!3dv(v[id[i] [1] ] ) ;
glVertex3dv(v[id[i] [1] ] ) ;
glNorma!3dv(v[id[i] [2] ] ) ;
glVertex3dv(v[id[i] [2] ] ) ;
}
glEnd() ;
//====== Конец списка команд
glEndList ();
}
Точное вычисление нормалей
Проверьте результат и обсудите качество. В данном варианте нормали в вершинах заданы так, как будто изображаемой фигурой является сфера, а не икосаэдр. Это достаточно грубое приближение. Если поверхность произвольного вида составлена из треугольников, то вектор нормали к поверхности каждого из них можно вычислить точно, опираясь на данные о координатах вершин треугольника. Из $ курса векторной алгебры вы, вероятно, помните, что векторное произведение двух векторов а и b определяется как вектор п, перпендикулярный к плоскости, в которой лежат исходные векторы. Величина его равна площади параллелограмма, построенного на векторах а и b как на сторонах, а направление определяется так, что векторы a, b и п образуют правую тройку. Последнее означает, что если представить наблюдателя на конце вектора п, то он видит поворот вектора а к вектору b, совершаемый по кратчайшему пути против часовой стрелки. На Рисунок 6.4. изображена нормаль п (правая тройка) при различной ориентации перемножаемых векторов а и b.
Вносим свет
Вносим свет
Пока нет освещения, все попытки внести трехмерный реализм обречены на неудачу. Свет отражается по нормали (перпендикуляру) к поверхности. Однако в OpenGL нормаль надо задавать в вершинах, так как в случае произвольной криволинейной поверхности направление нормали различно в каждой ее точке. Чем точнее вычислен вектор нормали, тем реалистичней изображение. Но это дело довольно тонкое. Для тех, кто не любит математику, то есть излишне напрягать свое мышление, — просто отвратительное. Примеры с автонормалями расслабляют и усыпляют бдительность, так как они скрывают детали реализации. Чтобы с ними работать, тоже надо прилагать усилия и правильно включать вычислители (evaluators). Смотри документацию по функциям giMap*. В нашем же случае все просто. Нормали уже вычислены, осталось включить свет. Сделайте это, вставив изменения в тело функции init. Включите еще два параметра в конечном автомате (state machine) OpenGL.
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
Задайте некоторый поворот, например double gdAngleX=15, gdAngleY=30, и запустите на выполнение. Изображение должно стать значительно лучше, но куда делся цвет куба? Свет исключил цвет. Дело в том, что теперь цвет каждого пиксела вычисляется по формуле, которая учитывает цвет материала поверхности, его отражающие и испускающие свойства, цвет самого света, его направление и законы распространения (точнее, затухания — attenuation). По умолчанию OpenGL учитывает только направление света, но не место расположения источника. По умолчанию же свет направлен вдоль оси Z. Обратите внимание на то, что индекс 0 в GL_LIGHTO означает, что мы включаем первый из GL_MAX_LIGHTS возможных источников света. Эта константа зависит от платформы. Давайте определим ее для нашей платформы. Вставьте такой фрагмент:
int Lights;
glGetIntegerv(GL_MAX_LIGHTS, &Lights);
_asm nор
внутрь функции Init (после строки glEnable(GL_LIGHTO);) и поставьте точку останова (F9) на строке __asm пор.
Примечание 1
Примечание 1
Ассемблерная вставка _asm пор упрощает просмотр значения переменных в окне Variables, так как не дает новых (и отвлекающих) элементов просмотра. Идея использования такого приема принадлежит Марине Полубенцевой, с которой мы сотрудничаем в Microsoft Authorized Education Center при ФПК СПбГТУ (www.Avalon.ru). В книге использовано еще несколько идей и технологических приемов, автором которых является Полубенцева.
Затем нажмите F5 (Go). Когда выполнение дойдет до точки останова, посмотрите в окно Variables и убедитесь в том, что Lights приняла значение 8. Если хотите, то используйте описанный прием в дальнейшем для выяснения многочисленных параметров и состояний OpenGL. Посмотрите справку по glGet, чтобы получить представление о количестве этих параметров. Теперь уберите отладочный код и включите еще один тумблер в машине состояний OpenGL — учет цвета материала. Для этого вставьте строку:
glEnable(GL_COLOR_MATERIAL) ;
в функцию Init и запустите приложение. Обратите внимание на отличие оттенков цвета разных граней. Они определяются OpenGL с учетом направления нормалей. Попробуйте изменить их направление и посмотрите, что получится.
Выбор способа вычисления нормалей
Выбор способа вычисления нормалей
Верните знаки на место и продолжим процесс приближения к шару. Прежде всего восстановим возможность точного вычисления нормалей и дадим пользователю возможность интерактивного выбора между приближенным и точным их вычислением. С этой целью введем глобальную переменную
//====== Флаг способа вычисления нормалей
bool gbSmooth = false;
которая будет помнить текущий способ вычисления нормалей, и сделаем так, чтобы каждое нажатие клавиши N инвертировало эту переменную и способ вычисления нормали. Введите в функцию main реакцию на нажатие клавиши N, вставив строку
auxKeyFunc(AUX_n, KeyN);
Реализацию функции обработки вставьте до функции main:
void _stdcall KeyN()
{
//====== Изменяем способ вычисления нормалей
gbSmooth = !gbSmooth;
11====== Заново создаем список команд
DrawScene(); }
Введите новую версию функции setTria, которая учитывает выбранный способ вычисления нормалей:
void setTria(double *vl, double *v2, double *v3)
{
glBegin(GLJTRIANGLES);
//====== Если выбран способ точного вычисления нормали
if (!gbSmooth)
{
//====== Правая тройка векторов
double dl[3], d2[3], norm[3];
//====== Вычисляем координаты векторов
//====== двух сторон треугольника
for (int j = 0; j.< 3; j++)
{
dl[j] = vl[j] - v2[j); d2[j] = v2[j] - v3[j];
}
//====== Вычисляем нормаль к плоскости
//====== треугольника со сторонами dl и d2
getNorm(dl, d2, norm);
glNormalSdv(norm);
glVertex3dv(vl);
glVertex3dv(v2);
glVertex3dv(v3);
}
else
{
//=== Неточное (приближенное) задание нормали
glNorma!3dv(vl);
glVertexSdv(vl);
glNorma!3dv(v2);
glVertex3dv(v2);
glNorraalSdv(v3);
glVertex-3dv(v3);
}
glEnd ();
}
Запустите и проверьте результат, нажимая клавишу N. Надеюсь, что теперь важность точного вычисления нормалей стала для вас еще более очевидной.
Чтение данных
Чтение данных
В теле следующей функции ReadData мы создадим файловый диалог, в контексте которого пользователь выбирает файл с новыми данными графика, затем вызовем функцию непосредственного чтения данных (DoRead) и создадим новую сцену на основе прочитанных данных. Попутно мы демонстрируем, как обрабатывать ошибки и работать с файловым диалогом, созданным с помощью функций API. Стандартный диалог открытия файла в этом случае более управляем, и ему можно придать множество сравнительно новых стилей. Стиль OFN_EXPLORER работает только в Windows 2000:
void COGView: : ReadData ()
{
//=== Строка, в которую будет помещен файловый путь
TCHAR szFile[MAX_PATH] = { 0 } ;
//====== Строка фильтров демонстрации файлов
TCHAR *szFilter =TEXT ("Graphics Files (*.dat)\0")
TEXT("*.dat\0")
TEXT ("All FilesNO")
TEXT ( " * . * \ 0 " ) ;
//====== Выявляем текущую директорию
TCHAR szCurDir[MAX_PATH] ;
: :GetCurrentDirectory (MAX_PATH-1, szCurDir) ;
//== Структура данных, используемая файловым диалогом
OPENFILENAME ofn;
ZeroMemory (&ofn,sizeof (OPENFILENAME) ) ;
//====== Установка параметров будущего диалога
ofn.lStructSize = sizeof (OPENFILENAME) ;
и * . *, текстовые описания которых можно увидеть в одном из окон стандартного диалога поиска и открытия файлов:
//====== Функция непосредственного чтения данных
bool COGView: : DoRead ( HANDLE hFile) {
//====== Сначала узнаем размер файла
DWORD nSize = GetFileSize (hFile, 0) ;
//=== Если не удалось определить размер, GetFileSize
//====== возвращает 0xFFFFFFFF
if (nSize == 0xFFFFFFFF)
{
GetLastError () ;
MessageBox (_T ("Некорректный размер файла"));
CloseHandle (hFile) ;
return false;
//=== Создаем временный буфер размером в весь файл BYTE
*buff = new BYTE [nSize+1] ;
//====== Обработка отказа выделить память
if (Ibuff) {
MessageBox (_T ("Слишком большой размер файла"))
CloseHandle (hFile) ;
return false;
//====== Реальный размер файла
DWORD nBytes;
//====== Попытка прочесть файл
ReadFile (hFile, buff, nSize, &nBytes, 0) ; CloseHandle (hFile) ;
//====== Если реально прочитано меньшее число байт
if (nSize != nBytes)
{
MessageBox (_T ("Ошибка при чтении файла"));
return false;
}
//====== Генерация точек изображения
SetGraphPoints (buff, nSize) ;
//====== Освобождение временного буфера
delete [] buff;
// ====== Возвращаем успех
return true;
}
В данный момент можно запустить приложение, и оно должно работать. В окне вы должны увидеть изображение поверхности, которое приведено на Рисунок 7.1. Для создания рисунка мы изменили цвет фона на белый, так как в книге этот вариант считается более предпочтительным. Попробуйте изменить размеры окна. Изображение поверхности должно пропорционально изменить свои размеры. Оцените качество интерполяции цветов внутренних точек примитивов и степень влияния освещения. Позже мы создадим диалог для управления параметрами света и отражающих свойств материала. А сейчас отметим, что напрашивается введение возможности управлять ориентацией и местоположением поверхности с помощью мыши. Для того чтобы убедиться в сложности автомата состояний OpenGL, a также в том, что все в нем взаимосвязано, временно поменяйте местами две строки программы: glVertexSf (xi, yi, zi); и glVertex3f (xn, yn, zn);. Вы найдете их в теле функции DrawScene.
Диалог по управлению светом
Диалог по управлению светом
В окне редактора диалогов (Resource View > Dialog > Контекстное меню > Insert Dialog) создайте окно диалога по управлению светом, которое должно иметь такой вид:
График по умолчанию
График по умолчанию
Пришла пора создать тестовую поверхность у = f (x, z), которую мы будем демонстрировать по умолчанию, то есть до того, как пользователь обратился к файловому диалогу и выбрал файл с данными, которые он хочет отобразить в окне OpenGL Функция Def aultGraphic, коды которой вы должны вставить в файл ChildView,cpp, задает поверхность, описываемую уравнением:
Yi,j=[3*п*(i-Nz/2)/2*Nz]*SIN[3*п*(j-Nx/2)/2*Nx]
Здесь п. обозначает количество ячеек сетки вдоль оси Z, а пх — количество ячеек вдоль оси X. Индексы i (0 < i < пz) и j (0 < j < nx) выполняют роль дискретных значений координат (Z, X) и обозначают местоположение текущей ячейки при пробеге по всем ячейкам сетки в порядке, описанном выше. Остальные константы подобраны экспериментально так, чтобы видеть полтора периода изменения гармонической функции.
Мы собираемся работать с двоичным файлом и хранить в нем информацию в своем формате. Формат опишем словесно: сначала следуют два целых числа m_xsize и m_zSize (размеры сетки), затем последовательность значений функции у = f (х, z) в том же порядке, в котором они были созданы. Перед тем как записать данные в файл, мы поместим их в буфер, то есть временный массив buff, каждый элемент которого имеет тип BYTE, то есть unsigned char. В буфер попадают значения переменных разных типов, что немного усложняет кодирование, но зато упрощает процесс записи и чтения, который может быть выполнен одной командой, так как мы пишем и читаем сразу весь буфер. В процессе размещения данных в буфер используются указатели разных типов, а также преобразование их типов:
void COGView::DefaultGraphic()
{
//====== Размеры сетки узлов
m xSize = m zSize = 33;
//====Число ячеек на единицу меньше числа узлов
UINTnz = m_zSize - 1, nx = m_xSize - 1;
// Размер файла в байтах для хранения значений функции
DWORD nSize = m_xSize * m_zSize * sizeof (float) + 2*sizeof (UINT) ;
//====== Временный буфер для хранения данных
BYTE *buff = new BYTE[nSize+l] ;
//====== Показываем на него указателем целого типа
UINT *p = (UINT*)buff;
//====== Размещаем данные целого типа
*р++ = m_xSize;
*р++ = m_zSize;
//====== Меняем тип указателя, так как дальше
//====== собираемся записывать вещественные числа
float *pf = (float*)?;
//=== Предварительно вычисляем коэффициенты уравнения
double fi = atan(l.)*6,
kx = fi/nx,
kz = fi/nz;
//====== В двойном цикле пробега по сетке узлов
//=== вычисляем и помещаем в буфер данные типа float
for (UINT i=0; i<ra_zSize;
for (UINT j=0; j<m_xSize;
{
*pf++ = float (sin(kz* (i-nz/2.) ) * sin (kx* (j-nx/2. ) )
}
}
//=== Переменная для того, чтобы узнать сколько
//=== байт было реально записано в файл DWORD nBytes;
//=== Создание и открытие файла данных sin.dat
HANDLE hFile = CreateFile (_T ("sin .dat") , GENERIC_WRITE,
0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0)
//====== Запись в файл всего буфера
WriteFile (hFile, (LPCVOID) buff, nSize, SnBytes, 0) ;
//====== Закрываем файл
CloseHandle (hFile) ;
//====== Создание динамического массива m_cPoints
SetGraphPoints (buff, nSize) ;
//====== Освобождаем временный буфер
delete [] buff;
}
В процессе создания, открытия и записи в файл мы пользуемся API-функциями CreateFile, WriteFile и CloseHandle, которые предоставляют значительно больше возможностей управлять файловых хозяйством, чем, например, методы класса CFile или функции из библиотек stdio.h или iostream.h. Обратитесь к документации, для того чтобы получить представление о них.
Настройка проекта
Настройка проекта
На странице VS Home Page выберите команду (гиперссылку) Create New Project.
В окне диалога New Project выберите уже знакомый вам тип проекта: MFC Application, задайте имя проекта OG и нажмите ОК.
В окне мастера MFC Application Wizard выберите вкладку Application Type и задайте такие настройки проекта: Single documents, MFC Standard, Document/View architecture support, Use MFC in a shared DLL.
Перейдите на страницу Advanced Features диалога и снимите флажки Printing and print preview, ActiveX Controls, так как мы не будем использовать эти возможности.
Нажмите кнопку Finish.
Этот тип стартовой заготовки позволяет работать с окном (cocview), которое помещено в клиентскую область окна-рамки (CMainFrame), и создать в этом окне контекст передачи OpenGL. Класс документа нам не понадобится, так как мы собираемся производить все файловые операции самостоятельно, используя свой собственный двоичный формат данных. В связи с этим нам не нужна помощь в сериализации данных, которую предоставляет документ. Для использования функций библиотеки OpenGL надо сообщить компоновщику, чтобы он подключил необходимые библиотеки OpenGL, на сей раз только две.
В окне открывшегося диалога OG Property Pages выберите элемент дерева Linker > Input.
Переведите фокус в поле Additional Inputs окна справа и добавьте в конец существующего текста имена файлов с описаниями трех библиотек: OPENGL32.LIB GLU32.LIB. Убедитесь в том, что все имена разделены пробелами и нажмите ОК.
Чтобы покончить с настройками общего характера, вставьте в конец файла StdAfx.h строки, которые обеспечивают видимость библиотеки OpenGL, а также некоторых ресурсов библиотеки STL:
#include <afxdlgs.h>
#include <raath.h>
//=== Подключение заголовков библиотек OpenGL
#include <gl/gl.h>
#include <gl/glu.h>
#include <vector>
using namespace std;
Параметры освещения
Параметры освещения
Установка параметрпв освещения осуществляется подобно тому, как это делалось в предыдущем уроке. Но здесь мы храним все параметры для тога, чтобы можно было управлять освещенностью изображения. Немного позже разработаем диалог, с помощью которого пользователь программы сможет изменять настройки освещения, а сейчас введите коды функции SetLight:
void COGView::SetLight()
{
//====== Обе поверхности изображения участвуют
//====== при вычислении цвета пикселов
//====== при учете параметров освещения
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1);
//====== Позиция источника освещения
//====== зависит от размеров объекта
float fPos[] =
{
(m_LightParam[0]-50)*m_fRangeX/100,
(m_LightParam[l]-50)*m_fRangeY/100,
(m_LightParam[2]-50)*m_fRangeZ/100,
l.f
};
glLightfv(GL_LIGHTO, GL_POSITION, fPos);
/1 ====== Интенсивность окружающего освещения
float f = m_LightParam[3]/100.f;
float fAmbient[4] = { f, f, f, O.f };
glLightfv(GL_LIGHTO, GL_AMBIENT, fAmbient);
//====== Интенсивность рассеянного света
f = m_LightParam[4]/100.f;
float fDiffuse[4] = { f, f, f, O.f };
glLightfv(GL_LIGHTO, GL_DIFFUSE, fDiffuse);
//====== Интенсивность отраженного света
f = m_LightParam[5]/100.f;
float fSpecular[4] = { f, f, f, 0.f };
glLightfv(GL_LIGHTO, GL_SPECULAR, fSpecular);
//====== Отражающие свойства материала
//====== для разных компонентов света
f = m_LightParam[6]/100.f;
float fAmbMat[4] = { f, f, f, 0.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, fAmbMat);
f = m_LightParam[7]/100.f;
float fDifMat[4] = { f, f, f, 1.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, fDifMat);
f = m_LightParam[8]/100.f;
float fSpecMat[4] = { f, f, f, 0.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, fSpecMat);
//====== Блесткость материала
float fShine = 128 * m_LightParam[9]/100.f;
glMaterialf(GL FRONT AND BACK, GL SHININESS, fShine);
//====== Излучение света материалом
f = m_LightParam[10]/100.f;
float f Emission [4] = { f , f , f , 0 . f } ;
glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, fEmission) ;
}
Подготовка изображения
Подготовка изображения
Разработаем код функции DrawScene, которая готовит и запоминает изображение на основе координат вершин, хранимых в контейнере m_cPoints. Изображение по выбору пользователя формируется либо в виде криволинейных четырехугольников (GL_QUADS), либо в виде полосы связанных четырехугольников (GL_QUAD_STRIP). Точки изображаемой поверхности расположены над регулярной координатной сеткой узлов в плоскости (X, Z). Размерность этой сетки хранится в переменных m_xSize и m_zSize. Несмотря на двухмерный характер сетки, для хранения координат вершин мы используем линейный (одномерный) контейнер m_cPoints, так как это существенно упрощает объявление контейнера и работу с ним. В частности, упрощаются файловые операции. Выбор четырех смежных точек генерируемого примитива (например, GL_QUADS) происходит с помощью четырех индексов (n, i, j, k). Индекс п последовательно пробегает по всем вершинам в порядке слева направо. Более точно алгоритм перебора вершин можно определить так: сначала проходим по сетке узлов вдоль оси X при Z = 0, затем увеличиваем Z и вновь проходим вдоль X и т. д. Индексы i, j, k вычисляются относительно индекса п. В ветви связанных четырехугольников (GL_QUAD_STRIP) работают только два индекса.
В контейнер m_cPoints данные попадают после того, как они будут прочитаны из файла. Для того чтобы при открытии приложения в его окне уже находился график функции, необходимо заранее создать файл с данными по умолчанию, открыть и прочесть его содержимое. Это будет сделано в коде функций
DefaultGraphic и SetGraphPoints. Алгоритм функции DrawScene разработан в предположении, что контейнер точек изображаемой поверхности уже существует. Флаг m_bQuad используется для выбора способа создания полигонов: в виде отдельных (GL_QUADS) или связанных (GL_QUAD_STRIP) четырехугольников. Позднее мы введем команду меню для управления этой регулировкой:
void COGView: : DrawScene ()
{
//====== Создание списка рисующих команд
glNewList (1, GL_COMPILE) ;
//====== Установка режима заполнения
//====== внутренних точек полигонов
glPolygonMode (GL_FRONT_AND_BACK, m_FillMode) ;
//====== Размеры изображаемого объекта
UINTnx = m_xSize-l, nz = m_zSize-l;
//====== Выбор способа создания полигонов
if (m_bQuad)
glBegin (GL_QUADS) ;
//====== Цикл прохода по слоям изображения (ось Z)
for (UINT z=0, i=0; z<nz; z++)
//====== Связанные полигоны начинаются
//====== на каждой полосе вновь
if (!m_bQuad)
glBegin (GLJ2UAD_STRIP) ;
//====== Цикл прохода вдоль оси X
for (UINT x=0; x<nx; x++)
// i, j, k, n — 4 индекса вершин примитива при
// обходе в направлении против часовой стрелки
int j = i + m_xSize, // Индекс узла с большим Z
k = j+1/ // Индекс узла по диагонали
n = i+1; // Индекс узла справа
//=== Выбор координат 4-х вершин из контейнера
float
xi = m_cPoints [i] .x,
yi = m_cPoints [i] .у,
zi = m_cPoints [i] . z,
xj = m_cPoints [ j ] .x,
yj = m_cPoints [ j ] .y,
zj = m_cPoints [ j ] . z,
xk = m_cPoints [k] .x,
yk = m_cPoints [k] .y,
zk = m cPoints [k] . z,
xn = m_cPoints [n] .x,
yn = m_cPoints [n] .y,
zn = m_cPoints [n] . z,
//=== Координаты векторов боковых сторон ах = xi-xn, ay = yi-yn,
by = yj-yi, bz = zj-zi,
//====== Вычисление вектора нормали
vx = ay*bz, vy = -bz*ax, vz = ax*by,
//====== Модуль нормали
v = float (sqrt (vx*vx + vy*vy + vz*vz) ) ;
//====== Нормировка вектора нормали
vx /= v; vy /= v; vz /= v;
//====== Задание вектора нормали
glNorma!3f (vx,vy,vz);
// Ветвь создания несвязанных четырехугольников
if (m_bQuad)
{
//==== Обход вершин осуществляется
//==== в направлении против часовой стрелки
glColorSf (0.2f, 0.8f, l.f);
glVertexSf (xi, yi, zi) ;
glColor3f (0.6f, 0.7f, l.f);
glVertexSf (xj, у j , zj);
glColorSf (0.7f, 0.9f, l.f);
glVertexSf (xk, yk, zk) ;
glColor3f (0.7f, 0.8f, l.f);
glVertexSf (xn, yn, zn) ;
}
else
//==== Ветвь создания цепочки четырехугольников
{
glColor3f (0.9f, 0.9f, l.0f);
glVertexSf (xn, yn, zn) ;
glColorSf (0.5f, 0.8f, l.0f);
glVertexSf (xj, у j , zj);
//====== Закрываем блок команд GL_QUAD_STRIP
if (!m_bQuad) glEnd ( ) ; } //====== Закрываем блок команд GL_QUADS
if (m_bQuad)
glEnd() ;
// ====== Закрываем список команд OpenGL
glEndList() ;
}
При анализе кода обратите внимание на тот факт, что вектор нормали вычисляется по упрощенной формуле, так как линии сетки узлов, над которой расположены вершины поверхности, параллельны осям координат (X, Z). В связи с этим равны нулю компоненты az и bх векторов боковых сторон в формуле для нормали (см. раздел «Точное вычисление нормалей» в предыдущем уроке).
Подготовка окна
Подготовка окна
Вы помните, что подготовку контекста передачи OpenGL надо рассматривать как некий обязательный ритуал, в котором порядок действий определен. В этой процедуре выделяют следующие шаги:
установка стиля окна; обработка сообщения WM_ERASEBACKGROUND и отказ от стирания фона; установка pixel-формата;создание контекста устройства (HDC) и контекста передачи (HGLRC);
специфическая обработка сообщения WM_SIZE;
обработка сообщения WM_PAINT;
освобождение контекстов при закрытии окна.
Как было отмечено ранее, окнам, которые в своей клиентской области используют контекст передачи OpenGL, при создании следует задать биты стиля WS_CLIPCHILDREN и ws_CLiPSiBLiNGS. Сделайте это внутри существующего тела функции PreCreateWindow класса cocview, добавив нужные биты стиля к тем, что устанавливаются в заготовке:
BOOL COGView::PreCreateWindow(CREATESTRUCT& cs)
{
//====== Добавляем биты стиля, нужные OpenGL
cs.style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN;
return CView::PreCreateWindow(cs);
}
Вы помните, что окно OpenGL не должно позволять Windows стирать свой фон, так как данная операция сильно тормозит работу конвейера. В связи с этим введите в функцию обработки WM_ERASEBKGND код, сообщающий системе, что сообщение уже обработано:
BOOL COGView::OnEraseBkgnd(CDC* pDC)
{
return TRUE;
}
Окно OpenGL имеет свой собственный формат пикселов. Нам следует выбрать и установить подходящий формат экранной поверхности в контексте устройства HDC, а затем создать контекст передачи изображения (HGLRC). Для описания формата пикселов экранной поверхности используется структура PIXELFORMATDESCRIPTOR. Выбор формата зависит от возможностей карты и намерений разработчика. Мы зададим в полях этой структуры такие настройки:
тип буферизации — двойной;
схему образования цвета RGBA;
количество бит для буфера глубины — 32;
поддержку регулировки прозрачностью и другие специфические настройки выключим.
В функцию OnCreate введите код подготовки окна OpenGL. Работа здесь ведется со структурой PIXELFORMATDESCRIPTOR. Кроме того, в ней создается контекст m_hRC и устанавливается в качестве текущего:
int COGView::OnCreate(LPCREATESTROCT IpCreateStruct)
{
if (CView::OnCreate(IpCreateStruct) == -1)
return -1;
PIXELFORMATDESCRIPTOR pfd = // Описатель формата
{
sizeof(PIXELFORMATDESCRIPTOR), // Размер структуры
1, // Номер версии
PFD_DRAW_TO_WINDOW | // Поддержка GDI
PFD_SUPPORT_OPENGL | // Поддержка OpenGL
PFD_DOUBLEBUFFER, // Двойная буферизация
PFD_TYPE_RGBA, // Формат RGBA, не палитра
24, // Количество плоскостей
//в каждом буфере цвета
24, 0, // Для компонента Red
24, 0, // Для компонента Green
24, 0, // Для компонента Blue
24, 0, // Для компонента Alpha
0, // Количество плоскостей
// буфера Accumulation
0, // То же для компонента Red
0, // для компонента Green
0, // для компонента Blue
0, // для компонента Alpha
32, // Глубина 2-буфера
0, // Глубина буфера Stencil
0, // Глубина буфера Auxiliary
0, // Теперь игнорируется
0, // Количество плоскостей
0, // Теперь игнорируется
0, // Цвет прозрачной маски
0 // Теперь игнорируется };
//====== Добываем дежурный контекст
m_hdc = ::GetDC(GetSafeHwnd());
//====== Просим выбрать ближайший совместимый формат
int iD = ChoosePixelForraat(m_hdc, spfd);
if ( !iD )
{
MessageBoxC'ChoosePixelFormat: :Error") ;
return -1;
}
//====== Пытаемся установить этот формат
if ( ISetPixelFormat (m_hdc, iD, Spfd) )
{
MessageBox("SetPixelFormat::Error");
return -1;
}
//====== Пытаемся создать контекст передачи OpenGL
if ( !(m_hRC = wglCreateContext (m_hdc)))
{
MessageBox("wglCreateContext::Error");
return -1;
}
//====== Пытаемся выбрать его в качестве текущего
if ( IwglMakeCurrent (m_hdc, m_hRC))
{
MessageBox("wglMakeCurrent::Error");
return -1;
//====== Теперь можно посылать команды OpenGL
glEnable(GL_LIGHTING); // Будет освещение
//====== Будет только один источник света
glEnable(GL_LIGHTO);
//====== Необходимо учитывать глубину (ось Z)
glEnable(GL_DEPTH_TEST);
//====== Необходимо учитывать цвет материала поверхности
glEnable(GL_COLOR_MATERIAL);
//====== Устанавливаем цвет фона .
SetBkColor () ;
//====== Создаем изображение и запоминаем в списке
DrawScene () ;
return 0;
}
Контекст передачи (rendering context) создается функцией wglCreateContext с учетом выбранного формата пикселов. Так осуществляется связь OpenGL с Windows. Создание контекста требует, чтобы обычный контекст существовал и был явно указан в параметре wglCreateContext. HGLRC использует тот же формат пикселов, что и НОС. Мы должны объявить контекст передачи в качестве текущего (current) и лишь после этого можем делать вызовы команд OpenGL, которые производят включение некоторых тумблеров в машине состояний OpenGL. Вызов функции DrawScene, создающей и запоминающей изображение, завершает обработку сообщения. Таким образом, сцена рассчитывается до того, как приходит сообщение о перерисовке WM_PAINT. Удалять контекст передачи надо после отсоединения его от потока. Это делается в момент, когда закрывается окно представления. Введите в тело заготовки OnDestroy следующие коды:
void COGView::OnDestroy(void)
{
//====== Останавливаем таймер анимации
KillTimer(1);
//====== Отсоединяем контекст от потока
wglMakeCurrent(0, 0); //====== Удаляем контекст
if (m_hRC)
{
wglDeleteContext(m_hRC);
m_hRC = 0;
}
CView::OnDestroy() ;
}
Так же как и в консольном проекте OpenGL, обработчик сообщения WM_SIZE должен заниматься установкой прямоугольника просмотра (giviewport) и мы, так же как и раньше, зададим его равным всей клиентской области окна. -Напомним, что конвейер OpenGL использует эту установку для того, чтобы поместить изображение в центр окна и растянуть или сжать его пропорционально размерам окна. Кроме того, в обработке onSize с помощью матрицы проецирования (GL_PROJECTION) задается тип проекции трехмерного изображения на плоское окно. Мы выбираем центральный или перспективный тип проецирования и задаем при этом угол зрения равным m_AngleView. В конструкторе ему было присвоено значение в 45 градусов:
void COGView::OnSize(UINT nType, int ex, int cy)
{
//====== Вызов родительской версии
CView::OnSize(nType, ex, cy) ;
//====== Вычисление диспропорций окна
double dAspect = cx<=cy ? double(cy)/ex : double(ex)/cy;
glMatrixMode (GL_PROJECTION) ;
glLoadldentity() ;
//====== Установка режима перспективной проекции
gluPerspective (m_AngleView, dAspect, 0.01, 10000.);
//====== Установка прямоугольника просмотра
glViewport(0, 0, сх, су);
}
Работа с контейнером
Работа с контейнером
Для работы с файлом мы пользовались буфером переменных типа BYTE. Для работы с данными в памяти значительно более удобной структурой данных является динамический контейнер. Мы, как вы помните, выбрали для этой цели контейнер, скроенный по шаблону vector. При заказе на его изготовление указали тип данных для хранения в контейнере. Это объекты класса CPointSD (точки трехмерного пространства). Мы пошли по простому пути и храним в файле только один компонент Y из трех координат точек поверхности в 3D. Остальные две координаты (узлов сетки на плоскости X-Z) будем генерировать на регулярной основе. Такой подход оправдан тем, что изображение OpenGL все равно претерпевает нормирующие преобразования, перед тем как попасть на двухмерный экран. Создание контейнера точек производится в теле функции SetGraphPoints, к разработке которой сейчас и приступим.
На вход функции подается временный буфер (и его размер), в который попали данные из файла. В настоящий момент в буфере находятся данные тестовой поверхности, а потом, при вызове из функции ReadData, в него действительно попадут данные из файла. Выбор данных из буфера происходит аналогично их записи. Здесь мы пользуемся адресной арифметикой, определяемой типом указателя. Так, операция ++ в применении к указателю типа UINT сдвигает его в памяти на sizeof (UINT) байт. Смена типа указателя (на float*) происходит в тот момент, когда выбраны данные о размерах сетки узлов.
Для надежности сначала проверяем данные из буфера на внутреннюю непротиворечивость в смысле размерностей. Затем мы уничтожаем данные контейнера и генерируем новые на основе содержимого буфера. В процессе генерации трехмерных координат точек их ординаты (Y) масштабируются для того, чтобы график имел пропорции, удобные для просмотра:
void COGView::SetGraphPoints(BYTE* buff, DWORD nSize)
{
//====== Готовимся к расшифровке данных буфера
//====== Указываем на него указателем целого типа
UINT *p = (UINT*)buff;
//=== Выбираем данные целого типа, сдвигая указатель
m_xSize = *р; m_zSize = *++p;
//====== Проверка на непротиворечивость
if (m_xSize<2 || m_zSize<2 ||
m_xSize*m_zSize*sizeof(float)
+ 2 * sizeof(UINT) != nSize)
{
MessageBox (_T ("Данные противоречивы") ) ;
return;
}
//====== Изменяем размер контейнера
//====== При этом его данные разрушаются
m_cPoints . resize (m_xSize*m_zSize) ;
if (m_cPoints .empty () )
{
MessageBox (_T ("He возможно разместить данные")
return;
}
//====== Подготовка к циклу пробега по буферу
//====== и процессу масштабирования
float x, z,
//====== Считываем первую ординату
*pf = (float*) ++р,
fMinY = *pf,
fMaxY = *pf,
right = (m_xSize-l) /2 . f ,
left = -right,
read = (m_zSize-l) /2 . f ,
front = -rear,
range = (right + rear) /2. f;
UINTi, j, n;
//====== Вычисление размаха изображаемого объекта
m_fRangeY = range;
m_fRangeX = float (m_xSize) ;
m_fRangeZ = float (m_zSize) ;
//====== Величина сдвига вдоль оси Z
m_zTrans = -1.5f * m_fRangeZ;
//====== Генерируем координаты сетки (X-Z)
//====== и совмещаем с ординатами Y из буфера
for (z=front, i=0, n=0; i<m_zSize; i++, z+=l.f)
{
for (x=left, j=0; j<m_xSize; j++, x+=l.f, n++)
{
MinMax (*pf, fMinY, fMaxY) ;
m_cPoints[n] = CPoint3D(x, z, *pf++) ;
}
}
//====== Масштабирование ординат
float zoom = fMaxY > fMinY ? range/ (fMaxY-fMinY)
: l.f;
for (n=0; n<m_xSize*m_zSize;n++)
{
m_cPoints [n] . у = zoom * (m_cPoints [n] . у - fMinY) - range/2. f;
}
}
При изменении размеров контейнера методом (resize) все его данные разрушаются. В двойном цикле пробега по узлам сетки мы восстанавливаем (генерируем заново) координаты X и Z всех вершин четырехугольников. В отдельном цикле пробега по всему контейнеру происходит масштабирование ординат (умножение на предварительно вычисленный коэффициент zoom). В используемом алгоритме необходимо искать экстремумы функции у = f (x, z). С этой целью удобно иметь глобальную функцию MinMax, которая корректирует значение минимума или максимума, если входной параметр превышает существующие на сей момент экстремумы. Введите тело этой функции в начало файла реализации оконного класса (ChildView.cpp):
inline void MinMax (float d, floats Min, float& Max)
{
//====== Корректируем переданные по ссылке параметры
if (d > Max)
Max = d; // Претендент на максимум
else if (d < Min)
Min = d; // Претендент на минимум
}
Реакции на сообщения Windows
Реакции на сообщения Windows
Вспомните, как вы ранее вводили в различные классы реакции на сообщения Windows и повторите эти действия для класса cOGView столько раз, сколько необходимо, чтобы в нем появились стартовые заготовки функций обработки следующих сообщений:
WM_DESTROY — окно исчезло с экрана, но не из памяти;
WM_ERASEBKGND — фон окна должен быть стерт;
WM_LBUTTONDOWN — нажата левая кнопка мыши;
WM_LBUTTONUP — отпущена левая кнопка мыши;
WM_MOUSEMOVE — курсор мыши перемещается;
WM_RBUTTONDOWN — нажата правая кнопка мыши;
WM_RBUTTONUP — отпущена правая кнопка мыши;
WM_SIZE — изменился размер окна;
WM_TIMER — истек квант времени какого-то из таймеров.
В конструктор класса вставьте код установки начальных значений переменных:
COGView::COGView()
{
//====== Контекст передачи пока отсутствует
m_hRC = 0;
//====== Начальный разворот изображения
m_AngleX = 35.f;
m_AngleY = 20.f;
//====== Угол зрения для матрицы проекции
m_AngleView = 45.f;
//====== Начальный цвет фона
m_BkClr = RGB(0, 0, 96);
// Начальный режим заполнения внутренних точек полигона
m_FillMode = GL_FILL;
//====== Подготовка графика по умолчанию
DefaultGraphic();
//====== Начальное смещение относительно центра сцены
//====== Сдвиг назад на полуторный размер объекта
m_zTrans = -1.5f*m_fRangeX;
m_xTrans = m_yTrans = 0.f;
//== Начальные значения квантов смещения (для анимации)
m_dx = m_dy = 0.f;
//====== Мышь не захвачена
m_bCaptured = false;
//====== Правая кнопка не была нажата
m_bRightButton = false;
//======
Реакция на сообщение о перерисовке
Реакция на сообщение о перерисовке
В функции перерисовки должна выполняться стандартная последовательность действий, которая стирает back-буфер и буфер глубины, корректирует матрицу моделирования, вызывает из списка команды рисования и по завершении рисования переключает передний и задний буферы. Полностью замените существующий текст функции OnDraw на тот, который приведен ниже: void COGView:: OnDtaw (CDC" pDC]
glClear<GL_COLOft_BUFFER_BIT | GL_BEPTH_3UFFER_BIT);
glMatrixMode(GLjtoDELVIEH) ;
glLoadldentitylT;
SetLight() ;
//=====Формировать
//===== Переключение буферов SwapBuffera
}
Рисуем четырехугольниками
Рисуем четырехугольниками
m_bQuad = true;
//====== Начальный значения параметров освещения
m_LightParam[0] = 50; // X position
m_LightParam[l] = 80; // Y position
m_LightParam[2] = 100; // Z position
m_LightParam[3] = 15; // Ambient light
m_LightParam[4] = 70; // Diffuse light
m_LightParam[5] = 100; // Specular light
m_LightParam[6] = 100; // Ambient material
m_LightParam[7] = 100; // Diffuse material
m_LightParam[8] = 40; // Specular material
m_LightParam[9] = 70; // Shininess material
m_LightParam[10] =0; // Emission material
}
Вид той же поверхности но освещенной справа
Рисунок 7.6. Вид той же поверхности, но освещенной справа
убедились, что использование списка команд OpenGL повышает эффективность передачи сложного изображения;
применили формулу вычисления нормали к поверхности и убедились в необходимости уделять этой проблеме достаточное внимание;
научились управлять освещенностью сцены OpenGL с помощью группы регуляторов;
оценили удобство управления группой регуляторов типа slider Control в функции обработки сообщения о прокрутке WM_HSCROLL.
Идентификаторы элементов управления
Таблица 7.1. Идентификаторы элементов управления
Элемент |
Идентификатор |
Диалог |
IDD_PROP |
Ползунок Ambient в группе Light |
IDC_AMBIENT |
Ползунок Diffuse в группе Light |
IDC_DIFFUSE |
Ползунок Specular в группе Light |
IDC_SPECULAR |
; Static Text справа от Ambient в группе Light |
IDC_AMB_TEXT |
, Static Text справа от Diffuse в группе Light |
IDC_DIFFUSE_TEXT |
Static Text справа от Specular в группе Light |
IDC_SPECULAR_TEXT |
Ползунок Ambient в группе Material |
IDC_AMBMAT |
Ползунок Diffuse в группе Material |
IDC_DIFFMAT |
' Ползунок Specular в группе Material |
IDC_SPECMAT |
f Static Text справа от Ambient в группе Material
|
IDC_AMBMAT_TEXT
|
:! Static Text справа от Diffuse. в группе Material |
IDC_DIFFMATJFEXT |
; Static Text справа от Specular в группе Material |
IDC_SPECMAT_TEXT |
Ползунок Shim'ness |
IDC_SHINE |
Ползунок Emission |
IDC_EMISSION |
« Static Text справа от Shininess |
IDC_SHINE_TEXT |
Static Text справа от Emission |
IDC_EMISSION_TEXT |
Ползунок X |
IDC_XPOS |
| Ползунок Y |
IDC_YPOS |
1 Ползунок Z |
IDC_ZPOS |
Static Text справа от X |
IDC_XPOS_TEXT |
Static Text справа от Y |
IDC_YPOS_TEXT |
Static Text справа от Z |
IDC_ZPOS_TEXT |
Кнопка Data File |
IDC_FILENAME |
Диалоговый класс
Для управления диалогом следует создать новый класс. Для этого можно воспользоваться контекстным меню, вызванным над формой диалога.
В левом окне диалога Add Class раскройте дерево Visual C++, сделайте выбор MFC > MFC Class и нажмите кнопку Open.
В окне мастера MFC Class Wizard задайте имя класса CPropDlg, в качестве базового класса выберите CDialog. При этом станет доступным ноле Dialog ID.
В это поле введите или выберите из выпадающего списка идентификатор шаблона диалога IDD_PROP и нажмите кнопку Finish.
Просмотрите объявление класса CPropDlg, которое должно появиться в новом окне PropDlg.h. Как видите, мастер сделал заготовку функции DoDataExchange для обмена данными с элементами управления на форме диалога. Однако она нам не понадобится, так как обмен данными будет производиться в другом стиле, характерном для приложений не MFC-происхождения. Такое решение выбрано в связи с тем, что мы собираемся перенести рассматриваемый код в приложение, созданное на основе библиотеки шаблонов ATL. Это будет сделано в уроке 9 при разработке элемента ActiveX, а сейчас введите в диалоговый класс новые данные. Они необходимы для эффективной работы с диалогом в немодальном режиме. Важным моментом в таких случаях является использование указателя на оконный класс. С его помощью легко управлять окном прямо из диалога. Мы слегка изменили конструктор и ввели вспомогательный метод GetsiiderNum. Изменения косметического характера вы обнаружите сами:
#pragma once
class COGView; // Упреждающее объявление
class CPropDlg : public CDialog
{
DECLARE_DYNAMIC(CPropDlg)
public:
COGView *m_pView; // Адрес представления
int m_Pos[ll]; // Массив позиций ползунков
CPropDlg(COGView* p) ;
virtual ~CPropDlg();
// Метод для выяснения ID активного ползунка int GetsiiderNum(HWND hwnd, UINT& nID) ;
enum { IDD = IDD_PROP };
protected: virtual void DoDataExchange(CDataExchange* pDX);
DECLARE_MESSAGE_MAP()
};
Откройте файл реализации диалогового класса и с учетом сказанного про адрес окна введите изменение в тело конструктора, который должен приобрести такой вид:
CPropDlg::CPropDlg(COGView* p)
: CDialog(CPropDlg::IDD, p)
{
//====== Запоминаем адрес объекта
m_pView = p;
}
Инициализация диалога
При каждом открытии диалога все его элементы управления должны отражать текущие состояния регулировок (положения движков), которые хранятся в классе представления. Обычно эти установки производят в коде функции OninitDialog. Введите в класс CPropDlg стартовую заготовку этой функции (CPropDlg > Properties > Overrides > OninitDialog > Add) и наполните ее кодами, как показано ниже:
BOOL CPropDlg: rOnlnitDialog (void)
{ CDialog: :OnInitDialog () ;
//====== Заполняем массив текущих параметров света
m_pView->GetLightParams (m _Pos) ;
//====== Массив идентификаторов ползунков
UINT IDs[] =
{
IDC_XPOS, IDC_YPOS, IDC_ZPOS,
IDC_AMBIENT,
IDC_DIFFUSE,
IDC_SPECULAR,
IDC_AMBMAT,
IDC_DIFFMAT,
IDC_SPECMAT,
IDC_SHINE,
IDCEMISSION
//====== Цикл прохода по всем регуляторам
for (int i=0; Ksizeof (IDs) /sizeof (IDs [ 0] ) ; i++)
{
//=== Добываем Windows-описатель окна ползунка H
WND hwnd = GetDlgItem(IDs[i] } ->GetSafeHwnd () ;
UINT nID;
//====== Определяем его идентификатор
int num = GetSliderNum(hwnd, nID) ;
// Требуем установить ползунок в положение m_Pos[i]
: :SendMessage(hwnd, TBM_SETPOS, TRUE, (LPARAM) m_Pos [i] )
char s [ 8 ] ;
//====== Готовим текстовый аналог текущей позиции
sprintf (s, "%d" ,m_Pos [ i] ) ;
//====== Помещаем текст в окно справа от ползунка
SetDlgltemText (nID, (LPCTSTR) s) ;
}
return TRUE;
}
Вспомогательная функция GetsliderNum по переданному ей описателю окна (hwnd ползунка) определяет идентификатор связанного с ним информационного окна (типа Static text) и возвращает индекс соответствующей ползунку пози ции в массиве регуляторов:
int CPropDlg: :GetSliderNum (HWND hwnd, UINT& nID)
{
//==== GetDlgCtrllD по известному hwnd определяет
//==== и возвращает идентификатор элемента управления
switch ( : : GetDlgCtrllD (hwnd) )
{
// ====== Выясняем идентификатор окна справа
case IDC_XPOS:
nID = IDC_XPOS_TEXT;
return 0;
case IDC_YPOS:
nID = IDC_YPOS_TEXT;
return 1;
case IDC_ZPOS:
nID = IDC_ZPOS_TEXT;
return 2;
case IDC_AMBIENT:
nID = IDC_AMB_TEXT;
return 3;
case IDC_DIFFUSE:
nID = IDC_DIFFUSE_TEXT;
return 4 ;
case IDC_SPECULAR:
nID = IDC_SPECULAR_TEXT;
return 5; case IDC_AMBMAT:
nID = IDC_AMBMAT_TEXT;
return 6 ;
case IDC_DIFFMAT:
nID = IDC_DIFFMAT_TEXT;
return 7 ;
case IDC_SPECMAT:
nID = IDC_SPECMAT_TEXT;
return 8 ; case IDC_SHINE:
nID = IDC_SHINE_TEXT;
return 9;
case IDC_EMISSION:
nID = IDC_EMISSION_TEXT;
return 10;
}
return 0;
}
Работа с группой регуляторов
В диалоговый класс введите обработчики сообщений WM_HSCROLL и WM_CLOSE, a также реакцию на нажатие кнопки IDC_FILENAME. Воспользуйтесь для этого окном Properties и его кнопками Messages и Events. В обработчик OnHScroll введите логику определения ползунка и управления им с помощью мыши и клавиш. Подобный код мы подробно рассматривали в уроке 4. Прочтите объяснения вновь, если это необходимо, Вместе с сообщением WM_HSCROLL система прислала нам адрес объекта класса GScrollBar, связанного с активным ползунком. Мы добываем Windows-описатель его окна (hwnd) и передаем его в функцию GetsliderNum, которая возвращает целочисленный индекс. Последний используется для доступа к массиву позиций ползунков. Кроме этого, система передает nSBCode, который соответствует сообщению об одном из множества событий, которые могут произойти с ползунком (например, управление клавишей левой стрелки — SB_LINELEFT). В зависимости от события мы выбираем для ползунка новую позицию:
void CPropDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
//====== Windows-описатель окна активного ползунка
HWND hwnd = pScrollBar->GetSafeHwnd();
UINT nID;
//=== Определяем индекс в массиве позиций ползунков
int num = GetSliderNum(hwnd, nID) ;
int delta, newPos;
//====== Анализируем код события
switch (nSBCode)
{
case SBJTHUMBTRACK:
case SB_THUMBPOSITION: // Управление мышью
m_Pos[num] = nPos;
break; case SB_LEFT: // Клавиша Home
delta = -100;
goto New_Pos; case SB_RIGHT: // Клавиша End
delta = + 100;
goto New__Pos; case SB_LINELEFT: // Клавиша <-
delta = -1;
goto New_Pos; case SB_LINERIGHT: // Клавиша ->
delta = +1;
goto New_Pos; case SB_PAGELEFT: // Клавиша PgUp
delta = -20;
goto New_Pos; case SB_PAGERIGHT: // Клавиша PgDn
delta = +20-;
goto New_Pos;
New_Pos: // Общая ветвь
//====== Устанавливаем новое значение регулятора
newPos = m_Pos[num] + delta;
//====== Ограничения
m_Pos[num] = newPos<0 ? 0 :
newPos>100 ? 100 : newPos;
break; case SB ENDSCROLL:
default:
return;
}
//====== Синхронизируем текстовый аналог позиции
char s [ 8 ] ;
sprintf (s, "%d",m__Pos [num] ) ;
SetDlgltemText (nID, (LPCTSTR)s);
//---- Передаем изменение в класс COGView
m_pView->SetLightParam (num, m_Pos [num] ) ;
}
Особенности немодального режима
Рассматриваемый диалог используется в качестве панели управления освещением сцены, поэтому он должен работать в немодальном режиме. Особенностью такого режима, как вы знаете, является то, что при закрытии диалога он сам должен позаботиться об освобождении памяти, выделенной под объект собственного класса. Эту задачу можно решить разными способами. Здесь мы покажем, как это делается в функции обработки сообщения WM_CLOSE. До того как уничтожено Windows-окно диалога, мы обнуляем указатель m_pDlg, который должен храниться в классе COGView и содержать адрес объекта диалогового класса. Затем вызываем родительскую версию функции OnClose, которая уничтожает Windows-окно. Только после этого мы можем освободить память, занимаемую объектом своего класса:
void CPropDlg: :OnClose (void)
{
//=== Обнуляем указатель на объект своего класса
m_pView->m_pDlg = 0;
//====== Уничтожаем окно
CDialog: :OnClose () ;
//====== Освобождаем память
delete this;
}
Реакция на нажатие кнопки IDC_FILENAME совсем проста, так как основную работу выполняет класс COGView. Мы лишь вызываем функцию, которая реализована в этом классе:
void CPropDlg:: OnClickedFilename (void)
{
//=== Открываем файловый диалог и читаем данные
m_pView->ReadData ( ) ;
}
Создание немодального диалога должно происходить в ответ на выбор команды меню Edit > Properties. Обычно объект диалогового класса, используемого в немодальном режиме, создается динамически. При этом предполагается, что класс родительского окна хранит указатель m_pDlg на объект класса диалога. Значение указателя обычно используется не только для управления им, но и как признак его наличия в данный момент. Это позволяет правильно обработать ситуацию, когда диалог уже существует и вновь приходит команда о его открытии. Введите в класс COGView новую public-переменную:
CPropDlg *m_pDlg; // Указатель на объект диалога
В начало файла заголовков OGView.h вставьте упреждающее объявление класса
CPropDlg:
class CPropDlg; // Упреждающее объявление
В конструктор COGView вставьте обнуление указателя:
m_pDlg =0; // Диалог отсутствует
Для обеспечения видимости класса CPropDlg дополните список директив препроцессора файла OGView.cpp директивой:
linclude "PropDlg.h"
Теперь можно ввести коды функции, которая создает диалог и запускает его вызовом функции Create (в отличие от DoModal для модального режима). Если происходит попытка повторного открытия диалога, то возможны два варианта развития событий:
команда открытия диалога недоступна, так как ее состояние зависит от значения указателя m_pDlg.
Реализуем первый вариант:
void COGView::OnEditProperties (void)
{
//====== Если диалог еще не открыт
if (!m_pDlg)
{
//=== Создаем его и запускаем в немодальном режиме
m_pDlg = new CPropDlg(this);
m_pDlg->Create(IDD_PROP);
}
else
// Иначе, переводим фокус в окно диалога
m_pDlg->SetActiveWindow();
}
Реакция на команду обновления пользовательского интерфейса при этом может быть такой:
void COGView::OnUpdateEditProperties(CCmdUI *pCmdUI)
{
pCmdUI->SetCheck (m_pDlg != 0);
}
Второй вариант потребует меньше усилий:
void COGView::OnEditProperties (void)
{
m_pDlg = new CPropDlg(this);
m_pDlg->Create(IDD_PROP); }
Но при этом необходима другая реакция на команду обновления интерфейса:
void COGView::OnUpdateEditProperties(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_pDlg == 0);
}
Выберите и реализуйте один из вариантов.
Панель управления
Завершая разработку приложения, вставьте в панель управления четыре кнопки
Для команд ID_EDIT_BACKGROUND, ID_EDIT_PROPERTIES, ID_VIEW_FILL И ID_VIEW_
QUAD. Заодно уберите из нее неиспользуемые нами кнопки с идентификаторами
ID_FILE_NEW, ID_FILE_OPEN, ID_FILE_SAVE, ID_FILE_PRINT, ID__EDIT_CUT,
ID_EDIT_COPY, ID_EDIT_PASTE. Запустите приложение, включите диалог Edit > Properties и попробуйте управлять регуляторами параметров света. Отметьте, что далеко не все из них отчетливым образом изменяют облик поверхности. Нажмите кнопку Data File, при этом должен открыться файловый диалог, но мы не сможем открыть никакого другого файла, кроме того, что был создан по умолчанию. Он имеет имя «Sin.dat» и должен находиться (и быть виден) в папке проекта. В качестве упражнения создайте какой-либо другой файл с данными, отражающими какую-либо поверхность в трехмерном пространстве. Вы можете воспользоваться для этой цели функцией DefaultGraphic, немного модифицировав ее код. На Рисунок 7.5 и 7.6 приведены поверхности, полученные таким способом. Вы можете видеть эффект, вносимый различными настройками параметров освещения.
Если вы тщательно протестируете поведение приложения, то обнаружите недостатки. Отметим один из них. Закрытые части изображения при некотором ракурсе просвечивают сквозь те части поверхности, которые находятся ближе к наблюдателю. Причину этого дефекта было достаточно трудно выявить. И здесь опять пришли на помощь молодые, талантливые слушатели Microsoft Authorized Educational Center (www.Avalon.ru) Кондрашов С. С. (scondor@rambler.ru) и Фролов Д. С. (dmfrolov@rambler.ru). Оказалось, что при задании типа проекции с помощью команды gluPerspective значения ближней границы фрустума не должны быть слишком маленькими:
gluPerspective (45., dAspect, 0.01, 10000.);
В нашем случае этот параметр равен 0.01. Замените его на 10. и сравните качество генерируемой поверхности.
Подведем итог. В этой главе мы:
вновь использовали стандартный контейнер объектов класса GPoint3D, который удобен для хранения вершин изображаемой поверхности;
Трехмерные графики функций
Трехмерные графики функций
Настройка проекта
Реакции на сообщения Windows
Подготовка окна
Работа с контейнером
Чтение данных
Управление изображением с помощью мыши
Включаем анимацию
Ввод новых команд
Диалог по управлению светом
В этой главе мы разработаем Windows-приложение, которое в контексте OpenGL изображает трехмерный график функции, заданной произвольным массивом чисел. Данные для графика могут быть прочтены из файла, на который указывает пользователь. Кроме этого, пользователь будет иметь возможность перемещать график вдоль трех пространственных осей, вращать его вокруг вертикальной и горизонтальной осей и просматривать как в обычном, так и скелетном режим. Регулируя параметры освещения поверхности, пользователь может добиться наибольшей реалистичности изображения, то есть усилить визуальный эффект трехмерного пространства на плоском экране.
Графики могут представлять собой результаты расчета какого-либо физического поля, например поверхности равной температуры, давления, скорости, индукции, напряжения и т. д. в части трехмерного пространства, называемой расчетной областью. Пользователь объекта должен заранее подготовить данные и записать их в определенном формате в файл. Объект по команде пользователя считывает данные, нормирует, масштабирует и изображает в своем окне, внедренном в окно приложения-клиента. Пользователь, манипулируя мышью, управляет местоположением и вращением графика, а открыв окно диалога Properties, изменяет другие его атрибуты.
Управление изображением с помощью мыши
Управление изображением с помощью мыши
Итак, мы собираемся управлять ориентацией изображения с помощью левой кнопки мыши. Перемещение курсора мыши при нажатой кнопке должно вращать изображение наиболее естественным образом, то есть горизонтальное перемещение должно происходить вокруг вертикальной оси Y, а вертикальное — вокруг горизонтальной оси X. Если одновременно с мышью нажата клавиша Ctrl, то мы будем перемещать (транслировать) изображение вдоль осей X и Y. С помощью правой кнопки будем перемещать изображение вдоль оси Z. Кроме того, с помощью левой кнопки мыши мы дадим возможность придать вращению постоянный характер. Для этого в обработчик WM_LBUTTONUP введем анализ на превышение квантом перемещения (m_dx, m_dy) некоторого порога чувствительности. Если он превышен, то мы запустим таймер, и дальнейшее вращение будем производить с его помощью. Если очередной квант перемещения ниже порога чувствительности, то мы остановим таймер, прекращая вращение. В обработке WM_MOUSEMOVE следует оценивать желаемую скорость вращения, которая является векторной величиной из двух компонентов и должна быть пропорциональна разности двух последовательных координат курсора. Такой алгоритм обеспечивает гибкое и довольно естественное управление ориентацией объекта. Начнем с обработки нажатия левой кнопки. Оно, очевидно, должно всегда останавливать таймер, запоминать факт нажатия кнопки и текущие координаты курсора мыши:
void COGView: :OnLButtonDown (UINT nFlags, CPoint point)
{
//====== Останавливаем таймер
KillTimer(1);
//====== Обнуляем кванты перемещения
m_dx = 0.f; m_dy = 0.f;
//====== Захватываем сообщения мыши,
//====== направляя их в свое окно
SetCapture ();
//====== Запоминаем факт захвата
m_bCaptured = true;
//====== Запоминаем координаты курсора
m_pt = point;
}
При нажатии на правую кнопку необходимо выполнить те же действия, что и при нажатии на левую, но дополнительно надо запомнить сам факт нажатия правой кнопки, с тем чтобы правильно интерпретировать последующие сообщения о перемещении указателя мыши и вместо вращения производить сдвиг вдоль оси Z:
void COGView::OnRButtonDown(UINT nFlags, CPoint point)
{
//====== Запоминаем факт нажатия правой кнопки
m_bRightButton = true;
//====== Воспроизводим реакцию на левую кнопку
OnLButtonDown(nFlags, point);
}
В обработчик отпускания левой кнопки мы вводим анализ на необходимость продолжения вращения с помощью таймера. В случае превышения порога чувствительности, запускаем таймер, сообщения от которого будут говорить, что надо продолжать вращение, поддерживая текущее значение скорости:
void COGView::OnLButtonUp(UINT nFlags, CPoint point)
{
//====== Если был захват,
if (m_bCaptured)
//=== то анализируем желаемый квант перемещения
//=== на превышение порога чувствительности
if (fabs(m_dx) > 0.5f || fabs(m_dy) > 0.5f)
//=== Включаем режим постоянного вращения
SetTimer(1,33,0);
else
//=== Выключаем режим постоянного вращения
KillTimer(1);
//====== Снимаем флаг захвата мыши
m_bCaptured = false;
//====== Отпускаем сообщения мыши
ReleaseCapture();
}
}
Отпускание правой кнопки должно просто отмечать факт прекращения перемещения вдоль оси Z и отпускать сообщения мыши для того, чтобы они работали на другие окна, в том числе и на наше окно-рамку. Если этого не сделать, то станет невозможным использование меню главного окна. Проверьте, если хотите. Для этого достаточно закомментировать вызов функции ReleaseCapture в обеих функциях:
void COGView::OnRButtonUp(UINT nFlags, CPoint point)
{
//====== Правая кнопка отпущена
m_bRightButton = false;
//====== Снимаем флаг захвата мыши
m_bCaptured = false;
//====== Отпускаем сообщения мыши
ReleaseCapture();
}
Теперь реализуем самую сложную часть алгоритма — реакцию на перемещение курсора. Здесь мы должны оценить желаемую скорость вращения. Она зависит от того, насколько резко пользователь подвинул объект, то есть оценить модуль разности двух последних позиций курсора, В этой же функции надо выделить случай одновременного нажатия служебной клавиши Ctrl Если она нажата, то интерпретация движения мыши при нажатой левой кнопке изменяется. Теперь вместо вращения мы должны сдвигать объект, то есть пропорционально изменять переменные m_xTrans и m_yTrans, которые затем подаются на вход функции glTranslate. Третья ветвь алгоритма обрабатывает движение указателя при нажатой правой кнопке. Здесь необходимо изменять значение переменной m_zTrans, обеспечивая сдвиг объекта вдоль оси Z. Числовые коэффициенты пропорциональности, которые вы видите в коде функции, влияют на чувствительность мыши и подбираются экспериментально. Вы можете изменить их на свой вкус так, чтобы добиться желаемой управляемости изображения:
void COGView::OnMouseMove(UINT nFlags, CPoint point)
{
if (m_bCaptured) // Если был захват,
{
// Вычисляем компоненты желаемой скорости вращения
m_dy = float (point .у - m_pt .у) /40 . f ;
m_dx = float (point .x - m_pt .x) /40. f ;
//====== Если одновременно была нажата Ctrl,
if (nFlags & MK_CONTROL)
{
//=== Изменяем коэффициенты сдвига изображения
m_xTrans += m_dx;
m_yTrans -= m_dy;
}
else
{
//====== Если была нажата правая кнопка
if (m_bRightButton)
//====== Усредняем величину сдвига
m_zTrans += (m_dx + m_dy)/2.f;
else
{
//====== Иначе, изменяем углы поворота
m_AngleX += m_dy;
m_AngleY += m_dx;
}
}
//=== В любом случае запоминаем новое положение мыши
m_pt = point; Invalidate (FALSE) ;
}
}
Запустите и проверьте управляемость объекта. Введите коррективы чувствительности на свой вкус. Попробуйте скорректировать эффект влияния поворота вокруг оси X на интерпретацию знака желаемого вращения вокруг оси Y. Здесь можно воспользоваться стеком матриц моделирования. Теперь добавим код в заготовку функции реакции на сообщения таймера с тем, чтобы ввести фиксацию состояния вращения.
Установка цвета фона
Установка цвета фона
Введите вспомогательную функцию, которая позволяет вычислить и изменить цвет фона окна OpenGL. Позже мы введем возможность выбора цвета фона с помощью стандартного диалога Windows по выбору цвета:
void COGView: :SetBkColor ()
{
//====== Расщепление цвета на три компонента
GLclampf red = GetRValue (m_BkClr) /255 . f ,
green = GetGValue (m_BkClr) /255. f ,
blue = GetBValue(m_BkClr) /255. f ;
//====== Установка цвета фона (стирания) окна
glClearColor (red, green, blue, 0.f);
//====== Непосредственное стирание
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) ;
}
Вид окна диалога по управлению параметрами света
Рисунок 7.4. Вид окна диалога по управлению параметрами света
Обратите внимание на то, что справа от каждого движка расположен элемент типа static Text, в окне которого будет отражено текущее положение движка в числовой форме. Три регулятора (элемента типа Slider Control) в левом верхнем углу окна диалога предназначены для управления свойствами света. Группа регуляторов справа от них поможет пользователю изменить координаты источника света. Группа регуляторов, объединенная рамкой (типа Group Box) с заголовком Material, служит для изменения отражающих свойств материала. Кнопка с надписью Data File позволит пользователю открыть файловый диалог и выбрать файл с данными для нового изображения. Для диалогов, предназначенных для работы в немодальном режиме, необходимо установить стиль Visible. Сделайте это в окне Properties > Behavior. Идентификаторы элементов управления мы сведем в табл. 7.1.
Вид освещенной поверхности в 3D
Рисунок 7.1. Вид освещенной поверхности в 3D
Вид поверхности освещенной слева
Рисунок 7.5. Вид поверхности, освещенной слева
Вид поверхности при использовании режима GL_QUAD_STRIP
Рисунок 7.2. Вид поверхности при использовании режима GL_QUAD_STRIP
Обработку следующей команды меню мы проведем в том же стиле, за исключением того, что переменная m_FillMode не является булевской, хоть и принимает лишь два значения (GL_FILL и GL_LINE). Из материала предыдущей главы помните, возможен еще одни режим изображения полигонов — GL_POINT. Логику его реализации при желании вы введете самостоятельно, а сейчас введите коды двух функции обработки команды меню:
void COGView::OnViewFill(void)
{
//=== Переключаем режим заполнения четырехугольника
m_FillMode = m_FillMode==GL_FILL ? GL_LINE : GL__FILL;
//====== Заново создаем изображение
DrawScene();
Invalidate(FALSE);
UpdateWindow() ;
}
void COGView::OnUpdateViewFill(CCmdUI *pCmdUI)
{
//====== Вставляем или убираем маркер выбора
pCmdUI->SetCheck(m_FillMode==GL_FILL) ;
}
Запустите и проверьте работу команд меню. Отметьте, что формула учета освещения работает и в случае каркасного изображения примитивов (Рисунок 7.3).
Вид поверхности созданной в режиме GL_LINE
Рисунок 7.3. Вид поверхности, созданной в режиме GL_LINE
Для обмена с диалогом по управлению освещением нам понадобятся две вспомогательные функции GetLightParams и SetLightParam. Назначение первой из которых заполнить массив переменных, отражающих текущее состояние параметров освещения сцены OpenGL. Затем этот массив мы передадим в метод диалогового класса для синхронизации движков (sliders) управления. Вторая функция позволяет изменить отдельный параметр и привести его в соответствие с положением движка. Так как мы насчитали 11 параметров, которыми хотим управлять, то придется ввести в окно диалога 11 регуляторов, которым соответствует массив m_LightPaxam из 11 элементов. Массив уже помещен в класс COGView, нам осталось лишь задействовать его:
void COGView: :GetLightParams (int *pPos)
{
//====== Проход по всем регулировкам
for (int i=0; i<ll; i++)
//====== Заполняем транспортный массив pPos
pPos[i] = m_LightParam[i] ;
void COGView: :SetLightParam (short Ip, int nPos)
{ //====== Синхронизируем параметр lp и
//====== устанавливаем его в положение nPos
m_LightParam[lp] = nPos;
//=== Перерисовываем представление с учетом изменений
Invalidate (FALSE) ;
}
Включаем анимацию
Включаем анимацию
Реакция на сообщение о том, что истек очередной квант времени в 33 миллисекунды (именно такую установку мы сделали в OnLButtonUp) выглядит очень просто. Увеличиваем углы поворота изображения на те кванты, которые вычислили в функции OnMouseMove и вызываем перерисовку окна. Так как при непрерывном вращении углы постоянно растут, то можно искусственно реализовать естественную их периодичность с циклом в 360 градусов. Однако с этой задачей успешно справляется OpenGL, и вы можете убрать код ограничения углов:
void COGView: :OnTimer (UINT nIDEvent)
{
//====== Если это был наш таймер
if (nIDEvent==l)
{
//====== Увеличиваем углы поворота
m_AngleX += m_dy;
m_AngleY += m_dx;
//====== Ограничители роста углов
if (m_AngleX > 360)
m_AngleX -= 360;
if (m_AngleX <-360)
m_AngleX += 360;
if (m_AngleY > 360)
m_AngleY -=360;
if (m_AngleY <-360)
m_AngleY +=360;
//====== Просим перерисовать окно
Invalidate(FALSE);
}
else
//=== Каркас приложения обработает другие таймеры
CView::OnTimer(nIDEvent);
}
Запустите и протестируйте приложение. Скорректируйте, если необходимо, коэффициенты чувствительности.
Вспомогательный класс
Вспомогательный класс
Нам вновь, как и в предыдущем уроке, понадобится класс, инкапсулирующий функциональность точки трехмерного пространства CPoint3D. Контейнер объектов этого класса будет хранить вершины изображаемой поверхности. В коде, который приведен ниже, присутствует слегка измененное по сравнению с предыдущим объявление класса CPoint3D, а также объявления новых данных и методов класса cocview. Заодно мы произвели упрощения стартового кода, которые обсуждались в уроке 5. Весь код введите в файл OGView.h вместо существующей в нем заготовки. Файл должен приобрести следующий вид1:
#pragma once
//========== Вспомогательный класс
class CPointSD
{
public: //====== Координаты точки
float x;
float у;
float z;
//====== Набор конструкторов
CPointSD ()
{
х = у - z = 0.f;
}
CPoint3D (float cl, float c2, float c3)
{
x = cl; z = c2; У = сЗ; ,
}
//====== Операция присвоения
CPoint3DS operator= (const CPointSDS pt)
x = pt.x; z = pt.z;
return *this;
У = pt.y;
//====== Конструктор копирования
CPointSD (const CPoint3D& pt)
{
*this = pt;
//=========== Класс окна OpenGL
class COGView :
public CView
{
protected:
COGView () ;
DECLARE_DYNCREATE(COGView)
public:
virtual ~COGView();
virtual void OnDraw(CDC* pDC) ;
virtual BOOL PreCreateWindow(CREATESTRUCT& cs) ,
//======= Новые данные класса
long m_BkClr; //
int m_LightParara[ll]; //
HGLRC m_hRC; //
HDC m_hdc; //
GLfloat m_AngleX; //
GLfloat m_AngleY; //
GLfloat m_AngleView; //
GLfloat m_fRangeX; //
GLfloat m_fRangeY; //
GLfloat m_fRangeZ; //
GLfloat m_dx; //
GLfloat m_dy; //
GLfloat m_xTrans; //
GLfloat m_yTrans; //
GLfloat m_zTrans; //
GLenura m_FillMode; //
bool m_bCaptured; //
bool m_bRightButton; //
bool m_bQuad; //
CPoint m_pt; //
UINT m_xSize; //
UINT m_zSize; //
//====== Массив вершин поверхности
vector <CPoint3D> m_cPoints;
//====== Новые методы класса
//=-==== Подготовка изображения
void DrawScene();
Цвет фона окна Параметры освещения Контекст OpenGL Контекст Windows Угол поворота вокруг оси X Угол поворота вокруг оси Y Угол перспективы Размер объекта вдоль X Размер объекта вдоль Y Размер объекта вдоль Z Квант смещения вдоль X Квант смещения вдоль Y Смещение вдоль X Смещение вдоль Y Смещение вдоль Z Режим заполнения полигонов Признак захвата мыши Флаг правой кнопки мыши Флаг использования GL_QUAD Текущая позиция мыши Текущий размер окна вдоль X Текущий размер окна вдоль Y
//====== Создание графика по умолчанию
void DefaultGraphic();
//====== Создание массива по данным из буфера
void SetGraphPoints(BYTE* buff, DWORD nSize);
//====== Установка параметров освещения
void SetLight();
//====== Изменение одного из параметров освещения
void SetLightParam (short lp, int nPos);
//====== Определение действующих параметров освещения
void GetLightParams(int *pPos); //====== Работа с файлом данных
void ReadData();
//====== Чтение данных из файла
bool DoRead(HANDLE hFile);
//====== Установка Работа с файлом данных
void SetBkColor();
DECLARE MESSAGE MAP()
Ввод новых команд
Ввод новых команд
Вы заметили, что до сих пор обходились без каких-либо ресурсов. Мы не учитываем традиционный диалог About, планку меню главного окна, панель инструментов, две таблицы (строк и ускорителей) и два значка, которые присутствовали в каркасе приложения изначально. Дальнейшее развитие потребует ввести новые ресурсы. Главным из них будет диалог, который мы запустим в немодальном режиме и который позволит подробно исследовать влияние параметров освещения на качество изображения. Начинать, как обычно, следует с команд меню. Скорректируйте меню главного окна так, чтобы в нем появились новые команды:
Edit > Background (ID_EDIT_BACKGROUND);
View > Fill (ID_VIEW_FILL);
View > Quad (ID_VIEW_QUAD).
Одновременно удалите не используемые нами команды: File > New, File > Open, File > Save, File > Save as, File > Recent File, Edit > Undo, Edit > Cut, Edit > Copy и Edit > Paste.
Примечание 1
Примечание 1
Вы, конечно, знаете, что идентификаторы команд можно не задавать. Они генерируются автоматически при перемещении фокуса от вновь созданной команды к любой другой.
После этого в классе cocview создайте обработчики всех новых команд с именами по умолчанию (их предлагает Studio.Net). При создании реакций на эти команды меню (COGView > Properties > Events) предварительно раскройте все необходимые элементы в дереве Properties t Commands. Одновременно с функциями обработки типа COMMAND создайте (для всех команд, кроме Edit > Background) функции обновления пользовательского интерфейса, то есть функции обработки типа UPDATE_ COMMANDJJI. Они, как вы помните, следят за состоянием команд меню и соответствующих им кнопок панели управления, обновляя интерфейс пользователя. Команды становятся доступными или, наоборот, в зависимости признака, управляемого програмистом.
В обработчике OnEditBackground мы вызовем стандартный диалог по выбору цвета, сразу открыв обе его страницы (см. флаг CC_FULLOPEN). С помощью этого диалога пользователь сможет изменить цвет фона:
void COGView::OnEditBackground(void)
{
//====== Создаем объект диалогового класса
CColorDialog dig(m_BkClr); //====== Устанавливаем бит стиля
dig.m_cc.Flags |= CC_FULLOPEN;
//====== Запускаем диалог и выбираем результат
if (cilg.DoModal ()==IDOK)
{
m_BkClr = dig.m_cc.rgbResuit;
//====== Изменяем цвет фона
SetBkColor();
Invalidate(FALSE);
}
}
Проверьте результат, запустив приложение и вызвав диалог. При желании создайте глобальный массив с 16 любимыми цветами и присвойте его адрес переменной lpCustColors, которая входит в состав полей структуры m_сс, являющейся членом класса CColorDialog. В этом случае пользователь сможет подобрать и запомнить некоторые цвета.
В обработчик OnViewQuad введите коды, инвертирующие булевский признак m_bQuad, который мы используем как флаг необходимости рисования отдельными четырехугольниками (GL_QUADS), и заново создают изображение. Если признак инвертирован, то мы рисуем полосами (GL_QUAD_STRIP):
void COGView::OnViewQuad(void)
{
// Инвертируем признак стиля задания четырехугольников
m_bQuad = ! m_bQuad;
//====== Заново создаем изображение
DrawScene (); Invalidate(FALSE); UpdateWindow();
}
В обработчик команды обновления интерфейса введите коды, которые обеспечивают появление маркера выбора рядом с командой меню (или залипания кнопки панели управления):
void COGView::OnUpdateViewQuad(CCmdUI* pCmdUI)
{
//====== Вставляем или убираем маркер (пометку)
pCmdUI->SetCheck(m_bQuad==true);
}
Проверьте результат и попробуйте объяснить зубчатые края поверхности (Рисунок 7.2). Не знаю, правильно ли я поступаю, когда по ходу изложения вставляю задачи подобного рода. Но мной движет желание немного приоткрыть дверь в кухню разработчика и показать, что все не так уж просто. Искать ошибки в алгоритме, особенно чужом, является очень кропотливым занятием. Однако совершенно необходимо приобрести этот навык, так как без него невозможна работа в команде, а также восприятие новых технологий, раскрываемых в основном посредством анализа содержательных (чужих) примеров (Samples). Чтобы обнаружить ошибку подобного рода, надо тщательно проанализировать код, в котором создается изображение (ветвь GL_QUAD_STRIP), и понять, что неправильно выбран индекс вершины. Замените строку givertex3f (xn, yn, zn); HaglVertexSf (xi, yi, zi); и вновь проверьте работу приложения. Зубчатость края должна исчезнуть, но в алгоритме, тем не менее, осталась еще небольшая, слабо заметная неточность. Ее обнаружение и исправление я оставляю вам, дорогой читатель.