Учебник по Visual C++ .Net

         

Двойная буферизация


В настоящий момент перерисовка изображения во время манипуляций мышью очень плохая, так как мы работаем с одним (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, для того чтобы в следующей главе разработать Windows-приложение, которое можно рассматривать как инструмент просмотра результатов научных расчетов. Материал этого раздела позволит вам постепенно войти в курс дела и овладеть очень привлекательной технологией создания и управления трехмерными изображениями. Сначала мы рассмотрим основные возможности библиотеки OpenGL, затем научимся управлять функциями OpenGL на примере простых приложений консольного типа и лишь после этого приступим к разработке Windows-приложения.



Интерактивное управление положением


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_MOUSEXJ;

int у = pEvent->data[AUX_MOUSEY];

на

short x = pEvent->data[AUX_MOUSEX];

short у = pEvent->data[AUX_MOUSEY];

в функциях OnLMouseMove и OnRMouseMove. Теперь повторите опыт и убедитесь в том, что, переходя через границу окна, координата х изменяется монотонно и приобретает отрицательные значения. Чтобы быть последовательным, замените тип глобальных данных для хранения текущей позиции курсора мыши. Вместо int giX, giY; вставьте short giX, giY;. Объяснение эффекта мы оставляем читателю в качестве упражнения по информатике.



Интерполяция цвета Вы можете запустить


(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.) позволяет масштабировать и нормировать компоненты цвета. Запустите и проверьте качество интерполяции цветов.



Использование списков С кубиком


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).

Рис. 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;


Как убирать внутренние линии Каждой


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 существует ряд приемов (поддержанных функциями), которые основаны на использовании заранее заготовленных массивов, а также списков команд 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 функций:

GIVertexPointer — задает адрес массива координат вершин;

GINormalPointer — задает адрес массива нормалей в вершинах;

GlColorPointer — задает адрес массива цветов, связанных с вершинами;

GlTexCoordPointer — задает адрес массива координат текстуры материала, задаваемой в вершинах;


GlEdgeFlagPointer — задает адрес массива флагов видимости линий, исходящих из вершины;

GllndexPointer — задает адрес массива цветовых индексов вершин в режиме цветовой палитры, а не RGBA.

Другой массив индексов — indices, определяет порядок выбора элементов из этих шести массивов. Но этого мало — надо произвести еще некоторые настройки в машине состояний OpenGL. Для перевода ее в режим использования массивов надо несколько раз вызвать функцию glEnableClientstate. Каждый вызов включает один из шести рассмотренных режимов. Только после этого функция glDrawElements способна эффективно задать сразу все примитивы. Например, вызов:

glEnableClientState(GL_VERTEX_ARRAY);

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

Совместно с командой glDrawElements обычно используют тот способ повышения эффективности отображения примитивов, который мы уже используем. Речь идет о паре функций: glNewList, glEndList. Все команды 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 К сожалению


Схема конвейера 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) и данными о текстуре. Такая точка вместе с сопутствующей информацией называется фрагментом. Фрагмент изменяет соответствующий ему пиксел в буфере кадра, если он проходит пять тестов:

Pixel ownership-тест, который проверяет принадлежность контексту, то есть не закрыт ли фрагмент другим окном;



Scissor-тест, который проверяет принадлежность вырезаемому прямоугольнику, который задается функцией glScissor;

Alpha-тест, который проверяет четвертый компонент цвета — прозрачность фрагмента с помощью функции glAlphaFunc;

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

Depth-buffer-тест, который проверяет, не закрыт ли фрагмент другим фрагментом с меньшей координатой Z.

Кроме того, фрагмент претерпевает другие изменения.

текстурирование — это генерация текстурного элемента (texel) на основе texture memory;

вычисление дымки (fog);

смешивание (blending);

интерполяция цвета (dithering);

логические операции;

маскирование с помощью трафарета (bitmask).


Основные этапы Для того чтобы


На примере многочисленных хранителей экрана (screen-saver) вы видели, как гладко работает анимация в OpenGL. OpenGL использует два буфера памяти (front and back). Первый (front-буфер) отображается на экране, второй в это время может обрабатываться процессором. Когда обработка закончится, то есть очередная сцена будет готова, вы можете произвести быстрое переключение буферов (swap), обеспечивая тем самым гладкую анимацию изображения. При обмене копирование массивов не происходит, изменяется лишь значение указателя (адреса) отображаемого блока памяти. Отметьте, что процесс рисования в back-буфер происходит быстрее, чем в front, так как большинство видеокарт запрещают редактировать изображение в момент вертикальной развертки, а это происходит 60-90 раз в секунду.

Рассмотрим основную схему алгоритма анимации, используемого в OpenGL-при-ложениях. В кино эффект движения достигается тем, что каждый кадр проецируется на экран в течение короткого промежутка времени, затем шторка проектора моментально закрывается, пленка продвигается на один кадр, вновь открывается шторка и цикл повторяется. Период цикла равен 1/24 с или даже 1/48 с в современных кинопроекторах. Современные компьютеры допускают смену кадра (refresh rate) до 120 раз в секунду. Рассмотренный алгоритм можно записать так.

в цикле по всем кадрам:

сотри старое изображение;

создай новое изображение;

задержи изображение на какое-то время.

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

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


Сотри старое изображение в back-буфере.

Создай в нем новое изображение.

Переключи буферы (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.


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


В ортографической проекции .(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);

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

Сначала с помощью команды glMatrixMode (GL_MODELVIEW); матрица моделирования выбирается в качестве текущей. Обычно при этом она сразу инициализируется единичной матрицей (команда glLoadldentity).

После этого текущая (единичная) матрица последовательно домножается справа на матрицы преобразования системы координат, которые формируются с помощью команд 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

Вы должны помнить, что вершины всех примитивов в 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]]);

}

glEndf) ;

glFlush ();

}

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

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

double gdAngleX=20, gdAngleY=20;

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


Подготовка окна Подготовку контекста


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-реализация


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 не рекомендует пользоваться ею для разработки коммерческих проектов, так как она содержит код цикла обработки сообщений, в который невозможно вставить обработку других произвольных сообщений.

Тип GLbyte эквивалентен типу signed char, a GLdouble — типу double. Свои собственные типы используются в целях упрощения переносимости на другие платформы. Список типов OpenGL мы приведем ниже. Четвертый компонент цвета определяет прозрачность цвета, то есть способ смешивания цвета фона с цветом изображения. Некоторые команды OpenGL имеют в конце символ v, который указывает, что ее аргументом должен быть адрес массива (вектора). Вектор в математике — это последовательность чисел (координат), единственным образом задающих элемент векторного пространства. Многие команды имеют несколько версий, позволяя в конечном счете задать вектор разными способами.

Около двадцати Windows GDI-функций создано специально для работы с OpenGL. Большая часть из них имеет префикс wgl (аббревиатура от Windows GL). Эти функции являются аналогами функций с префиксом glx, которые подключают OpenGL к платформе X window System. Наконец, существует несколько Win32-функций для управления форматом пикселов и двойной буферизацией. Они применимы только для специализированных окон OpenGL.



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


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 и запустите на выполнение.

Не знаю, как объяснить, но в Visual Studio б этот код почему-то работает, не-— смотря на явный промах, который типичен не только для начинающих программистов. Опытный читатель, конечно же, заметил, что мы создаем новые списки изображений, не уничтожая старые. Такие действия классифицируются как утечка памяти (memory lickage). Для ее устранения вставьте следующий фрагмент в функцию DrawScene перед вызовом glNewList:

//====== Если существует 1-й список,

if (gllsList(1))

//====== то освобождаем память

glDeleteLists (1,1);

Разъяснения можно найти в справке по функциям gllsList и glDeleteLists. He ошибитесь при выборе места вставки фрагмента, так как операции с памятью имеют особую важность. Запустите приложение и, нажимая на пробел, наблюдайте за изменением изображения, которое сначала приближается к сфере, затем постепенно возвращает свой первоначальный облик икосаэдра. Периодически нажимайте клавишу N для того, чтобы оценить влияние точного вычисления нормалей.


Штриховка линий Основные действия


OnDraw()

{

//====== Стираем окно

glClear (GL_COLOR_BUFFER_BIT);

//====== Цвет фона (синеватый)

glColor3f (0.3f, 0.3f, 1.);

//== Рисуем сначала unstippled rectangle (без узора)

//== Rect - это тоже полигон

glRectf (20., 20., 115., 120.);

glColor3f (1., 0., 0.); // Меняем цвет на красный

glEnable (GL_POLYGON_STIPPLE); // Включаем штриховку

glPolygonStipple (gStrip); // Задаем узор

glRectf (120., 20., 215., 120.); // Рисуем

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 OnDrawf)

{

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() ;

}


Создание консольного проекта Для


<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

В левой части равенств стоят оконные координаты:

(X, Y) — это координаты изображаемого объекта. Мы будем задавать их при формировании граничных точек линий командами glvertex2d;

(Хо, 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.


Создание сферы Для иллюстрации


Разбиение сферы на треугольники

Мы будем управлять степенью дискретизации сферы с помощью двух чисел: количества колец (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 отключите. Эти действия производятся так:

Поставьте фокус на элемент дерева OG.cpp в окне Solution Explorer и нажмите клавишу Delete. При этом файл будет отключен от проекта, но он останется в папке проекта.

Переведите фокус на строку 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 (глаз). Остальные индексы в применении к материалу обозначают различные компоненты его отражающих свойств.

Mа — коэффициент отражения окружающего (ambient) света,

Md — коэффициент отражения рассеянного (diffuse) отражения,

Ms — коэффициент отражения зеркального (specular) отражения,

N— вектор нормали вершины, который задан командой glNormal,

V1— нормированный вектор, направленный от вершины к источнику света,

Ve — нормированный вектор, направленный от вершины к глазу наблюдателя,

h — блесткость (shininess) материала.

Члены в круглых скобках — это скалярные произведения векторов. Если они дают отрицательные значения, то конвейер заменяет их нулем. Alpha-компонент результирующего цвета освещения устанавливается равным alpha-компоненту диффузного отражения материала. Так как мы задали лишь один источник света (LIGHTO), то знак суммы можно опустить. Обратите внимание на то, что блесткость материала уменьшает (обостряет) пятно отраженного света, так как возведение в степень h > 1 чисел (v, -v,), меньших единицы, уменьшает их значение. Параллельные векторы v, и v, дадут максимальный вклад. Чем больше их рассогласование, тем меньший вклад даст последний член формулы.

Ориентация поверхности

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



Вы можете реверсировать эту установку, задав режим 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. Ее параметры указывают:



тип геометрических примитивов (GL_TRIANGLES);

размер массива индексов, описывающих порядок выбора вершин (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. Северный и южный полюса обрабатываются индивидуально. Для осуществления обхода предварительно создаются константы:

da — шаг изменения сферического угла а (широта),

db — шаг изменения сферического угла b (долгота),

af и bf — конечные значения углов.

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

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

Координаты вершин (х, 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 + 1J.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) ;

}

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


Строим икосаэдр


Для иллюстрации работы с массивами вершин создадим более сложный объект — икосаэдр. Это такой дссятистенный дом с острой пятиугольной крышей и таким же полом, но углы пола смещены (повернуты) на л/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.




Рис. 6.2. Ориентация вектора нормали

Если координаты векторов а и b известны, то координаты нормали вычисляю по следующим формулам. Длина вектора нормали п зависит от длин вектор сомножителей и величины угла между ними:

Nx=AxBz-AzBy

Ny=AzBx-AxBz

Nz=AxBy-AyBx

Можно потерять много времени на осознание того факта, что не только правление нормали, но и ее модуль влияют на величину освещенности (и та) вершины, так как сопровождающая документация (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 () ;

}

Функцию нормировки всех нормалей можно возложить на автомат OpenGL, если включить состояние GL_NORMALIZE, но обычно это ведет к замедлению перерисовки и, как следствие, выполнения приложения, если изображение достаточно сложное. В нашем случае оно просто, и поэтому вы можете проверить действие настройки, если вставите вызов glEnable (GL_NORMALIZE); в функцию Init (до вызова OrawScene) и временно выключите вызов Scale(out); производимый в функции getNorm. Затем вернитесь к исходному состоянию.


Вносим свет


Пока нет освещения, все попытки внести трехмерный реализм обречены на неудачу. Свет отражается по нормали (перпендикуляру) к поверхности. Однако в 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 пор.

Ассемблерная вставка _asm пор упрощает просмотр значения переменных в окне Variables, так как не дает новых (и отвлекающих) элементов просмотра. Идея использования такого приема принадлежит Марине Полубенцевой, с которой мы сотрудничаем в Microsoft Authorized Education Center при ФПК СПбГТУ (www.Avalon.ru). В книге использовано еще несколько идей и технологических приемов, автором которых является Полубенцева.

Затем нажмите F5 (Go). Когда выполнение дойдет до точки останова, посмотрите в окно Variables и убедитесь в том, что Lights приняла значение 8. Если хотите, то используйте описанный прием в дальнейшем для выяснения многочисленных параметров и состояний OpenGL. Посмотрите справку по glGet, чтобы получить представление о количестве этих параметров. Теперь уберите отладочный код и включите еще один тумблер в машине состояний OpenGL — учет цвета материала. Для этого вставьте строку:

glEnable(GL_COLOR_MATERIAL) ;

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



Выбор способа вычисления нормалей


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. Надеюсь, что теперь важность точного вычисления нормалей стала для вас еще более очевидной.