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



Рисунок 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 отключите. Эти действия производятся так:

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


  2. Переведите фокус на строку Console того же окна и, вызвав контекстное меню, дайте команду Add New Item.
  3. Выберите шаблон 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), если направление обхода было обратным. В частности, ориентация поверхности влияет на ориентацию нормали.

Примечание 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. Ее параметры указывают:

  • тип геометрических примитивов (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 + 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) ;

}

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



Содержание раздела