ПРОГРАМИРУЕМ 3D ГРАФИКУ ИСПОЛЬЗУЯ DirectX

       

Создание фигур


Глава 4 Создание фигур

Геометрия


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

В механизме визуализации используется левосторонняя система координат (разумеется, проницательный читатель поднимает левую руку и вертит пальцами, показывая, куда направлены оси х, у и z). Если же вы проспали весь разговор о системах координат в главе 1, посмотрите на Рисунок 4-1 — на нем изображена левосторонняя система, которой мы будем пользоваться.

Рисунок. 4-1. Левосторонняя система координат


Геометрия ^¦ 89

Положительная часть оси у направлена вверх, х — вправо, a z — в глубь экрана, от пользователя.

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

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

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


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

Рисунок. 4-2. Оси координат в типичном масштабе





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

90

Глава 4. Создание фигур

Одна вершина, две вершины, получается собачка

Давайте нарисуем собачку. Возьмите карандаш и соедините точки на Рисунок 4-3, начиная с точки 1 и следуя по порядку чисел.

Рисунок. 4-3. Соедините точки



— Папа, что ты делаешь?

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

— Как просто — я так тоже могу.

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

Так в чем же суть всех наших рассуждений о собаках и точках? Дело в том, что компьютер умеет рисовать только прямые линии. Если мы захотим создать криволинейную фигуру, придется либо составлять ее из множества вершин, либо изобретать какой-нибудь другой способ для улучшения ее внешнего вида. Мы еще вернемся к рисованию криволинейных фигур в разделе «Создание твердых тел» на стр. 101. А пока достаточно запомнить на будущее определение вершин.



Геометрия ^^ 91

Векторы


Для определения вектора в трехмерном пространстве необходимо указать три координаты: х, у и z. Началом координат нашей системы является точка О, О, О. Рассмотрим точку в левом верхнем углу (и немного в глубь экрана), которая имеет координаты -2, 3, 4. Чтобы определить положение вершины, можно задать вектор, направленный из начала координат в точку трехмерного пространства. На Рисунок 4-4 изображен вектор, который определяет точку с координатами -2, 3, 4.

Рисунок. 4-4. Вектор, определяющий точку -2, 3, 4



Кроме того, с помощью вектора можно определить направление. Например, вектор О, 1, 0 определяет верхнее, то есть положительное направление оси Y (Рисунок 4-5).

В тех случаях, когда вектор используется для определения направления, его длина не имеет значения. Если хотя бы одна из координат вектора отлична от нуля, такой вектор однозначно задает направление. Тем не менее для определения направлений принято пользоваться единичными векторами. Длина единичного вектора равна 1, то есть:

x^+z2 = 1

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

92

Глава 4. Создание фигур

Рисунок. 4-5. Вектор, определяющий направление



ным вектором? При изменении ориентации объекта, единичные векторы упрощают вычисления с вершинами объекта. Тем не менее, вызывая функции механизма визуализации, вы не обязаны передавать им единичные векторы, поскольку перед использованием вектора он автоматически нормируется. Необходимость нормирования возникает только при проведении ваших собственных вычислений с векторами (другое распространенное применение единичных векторов заключается в определении нормали к плоскости; см. раздел «Нормали» на стр. 95).



Но довольно о векторах. Давайте подведем итог: вектор может определять положение вершины или направление.

Ориентация

Для того чтобы однозначно задать положение и ориентацию трехмерного объекта в пространстве, необходимы три вектора. Первый вектор определяет положение объекта (или, по крайней мере, некоторой эталонной точки объекта). Второй вектор определяет направление, в котором обращен объект. Для чего же нужен третий вектор? На Рисунок 4-6 изображены три объекта, все они имеют одинаковую форму и обращены в одном направлении. Чем они отличаются друг от друга?

Отличие состоит в том, что все эти объекты повернуты на разный угол вокруг своей оси. Чтобы полностью задать ориентацию объекта, необходимо дополнительно определить направление, которое для объекта будет считаться верхним. На Рисунок 4-7 изображены верхние векторы для всех трех фигур.

Три вектора однозначно определяют позицию и ориентацию объекта.

Геометрия

93

Грани

В нарисованной вами собачке (Рисунок 4-3 на стр. 91) множество вершин использовалось для определения одной грани неправильной формы. Трехмерные объекты состоят из нескольких граней, причем для компьютера эти грани являются абсолютно плоскими. Чтобы изобразить «гладкую» сферу, потребуется довольно много плоских граней. Грани могут иметь любую форму — от простейших треугольников до сложных многоугольников, так что вам удастся собрать свой трехмерный объект из треугольников, квадратов, пятиугольников и вообще из любых фигур.

Рисунок. 4-6. Три объекта, обращенные в одном направлении



Для окончательного определения ориентации объекта необходимо задать верхние векторы



94

Глава 4. Создание фигур

Следует учесть, что независимо от заданной формы граней механизм визуализации во время рисования обычно разбивает многоугольные грани на треугольники. Конечно, если в вашем компьютере установлена какая-нибудь сверхмощная видеокарта вместе с такими же мощными драйверами, она может обойтись без деления и рисовать многоугольники непосредственно на аппаратном уровне. Вы спросите, какое это имеет значение? Если вы захотите построить свою фигуру из каких-нибудь особенных многоугольников, чтобы сократить количество граней и тем самым ускорить ее прорисовку, возможно, разумнее с самого начала собирать ее из треугольников или других многоугольников с более простой формой — все равно ваш компьютер воспроизводит грани как совокупности треугольников. Кроме того, иногда это может приводить к довольно неожиданным эффектам. Если просто задать вершины многоугольника и воспроизвести его в окне, ваш «плоский» многоугольник может обрасти треугольными выступами. Причины такого явления мы рассмотрим чуть позже, в разделе «Создание простых фигур» на стр. 97. А пока попробуем построить наши объекты из треугольников и квадратов и посмотрим, что получится.



Последнее, о чем следует помнить — при задании вершин, определяющих грань, необходимо перечислять их по часовой стрелке (Рисунок 4-8). Это очень важно, поскольку механизм визуализации воспроизводит только одну сторону грани. Ограничиваясь лишь передней поверхностью граней, механизм визуализации избавляется от необходимости рисовать невидимые грани и тем самым экономит время. Будьте внимательны при задании вершин грани и следите за тем, чтобы грань была видна с нужной стороны.

Рисунок. 4-8. Порядок обхода вершин грани



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

Нормали

Нормалями называют людей, которые не станут заниматься геометрией ради собственного развлечения. Конечно, это шутка — например, я люблю геометрию, но при этом вполне нормален (я уверен в этом, потому что мне так говорили окружающие — честное слово!). Так что же такое нормаль?

Нормалью (по отношению к грани) называется вектор, определяющий ориентацию грани. На Рисунок 4-9 изображена грань вместе с нормалью к ней.

Геометрия '''^il 95

Рисунок. 4-9. Грань и нормаль



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

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



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

Предположим, у нас имеется прямоугольная грань и с каждой ее вершиной связан вектор нормали, как показано на Рисунок 4-10.

. Грань с нормалями вершин 96 ЯП' Глава 4. Создание фигур



На Рисунок 4-10 все нормали обращены вертикально вверх. При закраске по методу Гуро такая грань будет выглядеть плоской, так как все нормали вершин обращены в одном и том же направлении, и потому все точки грани, расположенные между вершинами, будут освещены одинаково. Возможно, это покажется вам неочевидным и даже непонятным, но давайте попробуем представить себе, что нормали определяют, насколько плоской выглядит грань у вершин. На Рисунок 4-10 все векторы направлены одинаково, поэтому вся грань выглядит плоской. Соответственно, ее освещенность остается постоянной.

Изменим положение нормалей и посмотрим, к" каким последствиям это приведет. На Рисунок 4-11 изображена та же самая грань с другими, неперпендикулярными нормалями*.

. Грань с неперпендикулярными нормалями



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

Если вы ничего не поняли, не огорчайтесь — сейчас мы продемонстрируем сказанное на примере программы, и тогда все станет гораздо понятнее.

Создание простых фигур

После небольшого экскурса в геометрию давайте займемся написанием программы, создающей несколько простых фигур. Данное приложение называется Shapes и находится в одноименном каталоге. Возможно, во время чтения стоит запустить приложение и посмотреть, как оно работает. На первый взгляд приложение Shapes ничем не отличается от Stage из главы 2, однако стоит щелкнуть в меню, как вы сразу увидите новые команды. Мы рассмотрим их назначение и реализацию. Весь интересующий нас код находится в файле MainFrm.cpp.



Начнем с исключительно простой фигуры — прямоугольника, который имеет четыре вершины, определяющие одну грань. Запустив приложение Shapes, вы найдете в меню Edit несколько команд с названиями фигур. Выберем команду Flat Face (то есть «плоская фигура»). Вот как выглядит ее программная реализация:

void CMainFrame::OnEditInsface() {

// Вставить простую плоскую грань

//и задать список вершин

D3DVECTOR vlist [] = (

На первый взгляд понятие «ненерпендикулярная нормаль» кажется внутренне протинорсчи-liliiM, однако если гопорить о нормалях к псршипам, а нс к граням, то ему можно придать смысл. — Иршчеч. пере».

Создание простых фигур <¦Ц 97

(-2, -2, -2},

t-2, -2, 2>,

{ 2, -2, 2},

{ 2, -2, -2} };

// Получить количество вершин в массиве

int nv = sizeof(vlist) / sizeof(D3DVECTOR);

// Определить вершины, входящие в каждую грань, // в формате:

// количество, вершина!, вершина2 и т. д. int flist [] = (4, О, 1, 2, 3, // 4 вершины, ...

О // Конец списка данных грани );

// Создать для фигуры новый макет NewScene ();

// Создать фигуру по списку вершин // и списку данных грани m pShape = new C3dShape;

m_p3hape-»Create (vlist, nv, flist);

// Присоединить фигуру к макету m_pScene-»AddChild(m_pShape) ;

}

Вершины грани содержатся в массиве векторов vlist. Каждый вектор определяет одну из четырех вершин, лежащих в плоскости у =-2. Прямоугольник имеет размеры 4х4 единицы. Первый вектор определяет ближнюю левую вершину, второй — дальнюю левую, третий — дальнюю правую и четвертый — ближнюю правую. Другими словами, мы обходим вершины по часовой стрелке.

Для определения фигуры используется целочисленный массив с информацией о грани, flist. Каждая грань в списке описывается количеством входящих в нее вершин, за которыми следует индекс каждой вершины в массиве vlist. Список граней завершается нулем. Он может содержать информацию о нескольких гранях, но в нашем случае определяется всего одна. Обратите внимание на то, что вершины пронумерованы в порядке О, 1, 2, 3, что соответствует их обходу по часовой стрелке, начиная с ближней левой, если смотреть на грань сверху. Поскольку грань находится в плоскости у =-2, она расположена ниже камеры (точки О, О, -10) и, следовательно, попадает в кадр.



Чтобы создать фигуру, следует вызвать функцию Create объекта C3dShape и передать ей в качестве аргументов список вершин, их общее количество и список данных грани. Не обращайте внимания на то, что происходит внутри объекта C3dShape, там нет ничего интересного — векторы и данные грани передаются в функцию механизма визуализации, которая и создает фигуру. Затем объект C3dShape присоединяется к макету, чтобы появиться в окне приложения. Если запустить приложение Shapes и выполнить команду Edit ¦ Flat Face, вы увидите что-нибудь похожее на Рисунок 4-12.

98 iy Глава 4. Создание фигур

Рисунок. 4-12. Приложение Shapes с одной плоской гранью



Обратите внимание на то, что грань выглядит плоской — это означает, что нормали ко всем вершинам имеют одинаковое направление. Вы можете проверить это командой View ¦ Normals, которая рисует возле каждой вершины небольшую стрелку, показывающую направление нормали. На Рисунок 4-13 изображена грань вместе с нормалями.

. Плоская грань с нормалями к вершинам



99

Создание простых фигур

Теперь давайте с теми же самыми данными создадим новую грань, но на этот раз укажем набор нормалей, направленных к центру грани. Ниже приведена функция для создания вогнутой грани, наподобие изображенной на Рисунок 4-11 на стр. 97:

void CMainFrame::OnEditDishface()

(

// Вогнутая грань D3DVECTOR vlist [] = (

(-2, -2, -2),

(-2, -2, 2),

( 2, -2, 2},

{ 2, -2, -2)

);

int nv = sizeof(vlist) / sizeof(D3DVECTOR);

D3DVECTOR nlist [] = {

( 1, 1, 1),

{ 1, 1, -1),

(-1, 1, -1),

{-1, 1, 1} };

int nn = sizeof(nlist) / sizeof(D3DVECTOR);

int flist [] = (4, О, О, 1, 1, 2, 2, 3, 3,

0 };

NewScene() ;

m_pShape = new C3dShape;

m_p3hape-»Create (vlist, nv, nlist, nn, flist);

m_pScene-»AddChild(m_pShape) ;

1

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



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

Результат выполнения этой функции изображен на Рисунок 4-14. Вы можете увидеть его на экране, для этого следует запустить приложение Shapes и выполнить команду Edit ¦ Dished Face.

Вы обратили внимание на то, что грань кажется вогнутой в середине? (Этот эффект особенно четко проявляется, если привести грань во вращение.) На случай, если вы забыли, напомню, что источник света находится в левом верхнем углу макета. С помощью приложения Shapes можно также убедиться в том, что

/b> lll^ Глава 4. Создание фигур

нормали, направленные за пределы грани, создают иллюзию выпуклости. Для того чтобы увидеть этот эффект, достаточно выполнить команду Edit ¦ Bulging Face.

Рисунок. 4-14. Грань с неперпендикулярными нормалями



Создание твердых тел

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

void CMainFrame::OnEditDefcube() (

D3DVECTOR vlist [] = {

{-1, -1, -1),

{-1, -1, 1},

{ 1, -1, 1},

{ 1, -1, -1),

{-1, 1, -1),

{-1, 1, 1),

{ 1, 1, 1),

{ 1, 1, -1) };

int nv = sizeof(vlist) / sizeof(D3DVECTOR);

int flist [] = (4, 0, 3, 2, 1,

4, 3, 7, 6, 2,

4, 4, 5, 6, 7,

4, 0, 1, 5, 4,

4, 0, 4, 7, 3,

4, 2, 6, 5, 1,

n

Создание твердых тел

/b>

};

NewScene() ;

m_pShape = new C3dShape;

m_pShape-»Create (vlist, nv, flist) ;

m_pScene-»AddChild(m_pShape) ;

} .

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



Рисунок. 4-15. Нумерация вершин куба



Прежде, чем читать дальше, следует самостоятельно убедиться в том, что вы поняли смысл описания граней. Может, на это придется потратить немного времени, зато в дальнейшем вы не будете создавать невидимые поверхности, ориентированные в неверном направлении. Функция OnEditDefcube строит фигуру, изображенную на Рисунок 4-16. Чтобы увидеть куб на экране, выполните команду Edit] Default Cube.

Как вы думаете, почему закраска граней выглядит такой однородной? Выполните команду View ¦ Normals, и вы увидите, что нормали к вершинам куба обращены от углов, по направлению от центра куба. Поскольку мы не стали задавать свои нормали, механизм визуализации создал их по умолчанию, усредняя нормали всех граней, прилегающих к вершине. Подобное решение может показаться до-

/h2>

Глава 4. Создание фигур

Рисунок. 4-16. Вращающийся куб с принятым по умолчанию расположением нормалей



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

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

void CMainFrame::OnEditFlatfacecube() {

D3DVECTOR vlist [] = {

(-1, -1, -1),

(-1, -1, 1),

( 1, -1, 1),

{ 1, -1, -1),

{-1, 1, -1),

{-1, 1, 1),

{ 1, 1, 1),

( 1, 1, -1} );

int nv = sizeof(vlist) / sizeof(D3DVECTOR);

D3DVECTOR nlist [] = { { i, 0, 0}, < 0, 1, 0), < 0, 0, 1), (-1, 0, 0),

Создание твердых тел

/h2>

{ 0, -1, 0), { 0, 0, -1}

);

int nn = sizeof(nlist) / sizeof(D3DVECTOR);

int flist [] = (4, 0, 4, 3, 4, 2, 4, 1, 4,

4, 3, 0, 7, 0, 6, 0, 2, 0, 4, 4, 1, 5, 1, 6, 1, 7, 1, 4, 0, 3, 1, 3, 5, 3, 4, 3, . 4, 0, 5, 4, 5, 7, 5, 3, 5, 4, 2, 2, 6, 2, 5, 2, 1, 2, 0



};

NewScene();

m_pShape = new C3dShape;

m_pShape-»Create (vlist, nv, nlist, nn, flist);

m_pScene-»AddChild(m_pShape) ;

t

Если запустить приложение и отобразить куб с плоскими гранями вместе с нормалями (сначала выполните команду Edit ¦ Flat-Faced Cube, затем — команду View ¦ Normals), вы увидите нечто похожее на Рисунок 4-17.

Рисунок. 4-17. Куб с плоскими гранями и векторами нормалей



Итак, теперь мы умеем создавать фигуры с плавными переходами граней (при которых нормали генерируются механизмом визуализации) или с более резкими переходами (при которых нормали задаются программистом). Иногда бывает нужно создать криволинейный объект, на котором присутствуют острые ребра. На Рисунок 4-18 изображен конус с закругленными сторонами и плоским основанием (команда Edit ¦ Cone).

/h2>

Глава 4. Создание фигур

Рисунок. 4-18. Конус



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

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

Существуют два основных способа для получения плоского основания и резкой границы между основанием и боковой поверхностью. Самое простое, что можно сделать, — это внести основание в список граней с отдельным набором вершин. Разумеется, координаты этих вершин должны совпадать с координатами нижних сторон боковых граней, иначе в фигуре появятся «дырки». Задавая отдельные вершины для нижней грани, мы тем самым указываем, что нижняя грань не имеет прилегающих граней; когда механизм визуализации будет генерировать нормали для вершин основания, он просто использует для этой цели нормаль основания. Такой подход приводит к желаемому результату (плоскому основанию с резким переходом), однако он немного расточителен, поскольку нам приходится задавать лишний набор вершин (в данном случае — 16).



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

Создание твердых тел

/h2>

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

Тела вращения

Тела вращения я впервые увидел в раннем детстве. Мой отец работал токарем, стоял целый день у токарного станка и точил фланцы, стержни, винты с нарезкой, трубы и т. д. (Рисунок 4-19). Токарный станок вращает металлическую заготовку, зажатую в патроне. К заготовке подносится резец, который срезает ненужный материал при вращении заготовки; положение резца определяет радиус детали. Продольное перемещение резца с одновременным вращением заготовки позволяет выточить стержень (гладкий или нарезной). Основная идея моего рассказа заключается в том, что твердый объект можно описать с помощью простой функции, определяющей радиус объекта в любой точке его длины.

Рисунок. 4-19. Токарный станок



Процесс построения тел вращения мало чем отличается от создания других трехмерных объектов. Необходимо задать набор вершин, определить, к каким граням они относятся, и затем при желании указать нормали. Поскольку тело вращения можно описать функцией зависимости радиуса от продольной координаты, нетрудно создать фрагмент программы, в котором такая функция используется для генерации вершин и данных граней. На Рисунок 4-20 изображено тело вращения, построенное именно этим способом. Чтобы увидеть тело вращения на экране, выполните команду Edit ¦ Solid of Revolution.

Ниже приводится фрагмент приложения Shapes, в котором создается изображенный на рисунке объект:



double RevolveFn(double z, void* pArg) (

if (z « -1.1) (

return sqrt(l — (z + 2)*(z + 2));

} else if (z » 0.8) (

/b> Щ^' Глава 4. Создание фигур

. Тело вращения



return sqrt(2 - (z - 2)*(z - 2));

} else (

return 0.5;

} }

// Создать тело вращения

void CMainFrame::OnEditSolidr()

{

// Создать объект по заданной функции

NewScene () ;

m_pShape = new C3dShape;

m_pShape-»CreateRSolid(-3.0, 2.2, 0.2, TRUE, TRUE, RevolveFn, NULL, 16);

m_pScene-»AddChiid(m_pShape) ;

// Развернуть объект, чтобы показать его со стороны m_pShape-»SetDirection(0, -1, 0) ;

)

Как видите, в данном случае мы имеем дело с двумя функциями. RevolveFn возвращает значения радиуса для заданной продольной координаты, а функция OnEditSolidr вызывается при выполнении команды меню Edit ¦ Solid of Revolution. Для построения объекта внутри функции OnEditSolidr вызываются функции RevolveFn и C3dShape::CreateRSolid. Аргументы CreateRSolid выглядят несколько необычно, поэтому позвольте мне объяснить их назначение.

Создавая функцию CreateRSolid, я не собирался делать ее универсальной. Вместо того чтобы обрабатывать координаты концов объекта, я решил всегда строить фигуру вдоль оси z. Таким образом, написанная вами функция (в данном

Тепа вращения

/h2>

случае RevolveFn) возвращает радиус объекта как функцию координаты по оси г. Аргументами CreateRSolid являются максимальное и минимальное значения координат по оси z, приращение по оси z, две логические величины, определяющие необходимость замыкания концов фигуры, указатель на функцию радиуса, необязательный аргумент, передаваемый в функцию радиуса, и наконец количество граней в круговой поверхности.

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

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



Давайте посмотрим, как работает функция C3dShape::CreateRSolid. Приведенный ниже фрагмент взят из библиотеки 3dPlus:

// Создание тел вращения

BOOL CBdShape::CreateRSolid(double zl, double z2, double dz, BOOL bClosedl, BOOL bClosed2, SOLIDRFN pfnRad, void* pArg, int nFacets) (

New () ;

ASSERT(pfnRad) ;

ASSERT(dz != 0) ;

int iZSteps = (int)((z2 - zl) / dz);

if (iZSteps « 1) return FALSE;

int iRSteps = nFacets;

if (iRSteps « 8) iRSteps = 8;

double da = _twopi / iRSteps;

// Создать массив для вершин int iVertices °= (iZSteps + 1) * iRSteps;

D3DVECTOR* Vertices = new D3DVECTOR [iVertices];

D3DVECTOR* pv - Vertices;

// Создать массив для данных граней.

// Каждая грань имеет 4 вершины, за исключением торцов.

int iFaces = iZSteps * iRSteps;

int iFaceEntries = iFaces * 5 + 1;

if (bClosedl) iFaceEntries += iRSteps + 1;

if (bClosed2) iFaceEntries += iRSteps + 1;

int* FaceData = new int [iFaceEntries] ;

int* pfd = FaceData;

// Заполнить координаты вершин double z = zl;

/b> ¦¦¦¦1' Глава 4. Создание фигур

double r, a;

for (int iZ = 0; iZ «= iZSteps; iZ++) { r = pfnRadfz, pArg) ;

a = 0;

for (int iR = 0; iR « iRSteps; iR++) {

pv-»x = D3DVAL(r * sin(a));

pv-»y = D3DVAL(r * cos(a));

pv-»z = D3DVAL(z) ;

pv++;

a += da;

} z += dz;

}

// Заполнить список граней

int iFirst = iRSteps;

for (iZ = 0; iZ « iZSteps; iZ++) {

for (int iR = 0; iR « iRSteps; iR++) {

*pfd++ =4; //No. of vertices per face

*pfd++ = iFirst + iR;

*pfd++ = iFirst + ((iR + 1) % iRSteps);

*pfd++ = iFirst - iRSteps +

((iR + 1) % iRSteps) ;

*pfd++ = iFirst - iRSteps + iR;

} iFirst += iRSteps;

} *pfd =0; // Завершить список

// Создать круговую поверхность с автоматической // генерацией нормалей

BOOL b = Create(Vertices, iVertices, NULL, 0, FaceData, TRUE) ;

delete [] FaceData;

FaceData = new int [iRSteps * 2 + 2] ;

D3DVECTOR nvect [] = { {0, 0, 1}, {О, 0, -1}

};

if (bClosedl) { pfd = FaceData;

*pfd++ = iRSteps;

for (int iR = 0; iR « iRSteps; iR++) (

*pfd++ = iR;

*pfd++ = 1;



}

Тела вращения тЩ^ 109

*pfd = 0;

m_hr = m_pIMeshBld-»AddFaces(iVertices, Vertices, 2, nvect,

ULONG*)FaceData, NULL) ;

ASSERT(SUCCEEDED(m_hr)) ;

}

if (bClosed2) { pfd = FaceData;

*pfd++ = iRSteps;

iFirst = iRSteps * iZSteps;

for (int iR = 0; iR « iRSteps; iR++) (

*pfd++ = iRSteps - 1 - iR + iFirst;

*pfd++ = 0;

}

*pfd = 0;

m hr = m pIMeshBld-»AddFaces (iVertices, Vertices, 2, nvect,

(ULONG*)FaceData, NULL) ;

ASSERT(SUCCEEDED(m_hr)) ;

}

delete [] Vertices;

delete [] FaceData;

m strMame = "Solid of revolution";

return b;

}

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

Возможно, вы обратили внимание на то, что фигуре присваивается имя (в данном случае — Solid of Revolution, хранящееся в переменной m_strName). Когда мы будем заниматься выбором объектов в макете, имя сообщит пользователю, какая фигура выбрана им в настоящий момент.

Мы очень кратко пробежались по большому фрагменту программы, и у вас наверняка осталось много вопросов по поводу его работы и назначению отдельных функций. Если вы хотите понять, как устроена функция CreateRSolid, возьмите лист бумаги в клетку, сверните его в трубку и затем представьте себе, что вам потребовалось описать каждое продольное ребро и каждую грань этой решетки. Именно это и происходит в приведенном выше фрагменте, а эксперимент с трубкой описывает мой подход к его написанию. Во фрагменте присутствуют несколько вызовов функции интерфейсов DirecQD, назначение которых можно узнать в документации по DirectX 2 SDK. Заодно найдите в SDK макрос D3DVAL и посмотрите, что он делает.

/b> ЩЦУ Глава 4. Создание фигур

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



Построение ландшафтов

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

Построение ландшафта по решетке

Давайте сначала рассмотрим алгоритм с решеткой. На Рисунок 4-21 изображен пример ландшафта, построенного по решетке из точек со случайной высотой (команда Edit ¦ Landscape 1).

Рисунок. 4-21. Случайный ландшафт, построенный с помощью решетки



Функция для построения поверхности, образующей ландшафт, напоминает функцию для создания тел вращения на стр. 108. В данном случае мы передаем функцию, которая генерирует высоту точки по значениям х и z. Ниже приведен фрагмент приложения Shapes:

Построение ландшафтов

double LandscapeFn(double x, double z, void* pArg)

{

return -2.0 + (double)(rand() % 100) / 100;

// Создать поверхность, изображающую ландшафт void CMainFrame::OnEditInsland()

{

// Создать поверхность с использованием функции высоты

NewScene() ;

m_pShape = new C3dShape;

m_p3hape-»CreateSurface (-5, 5, Т., -10, 10, 1, LandscapeFn, NULL) ;

m_pScene-»AddChild(m_pShape) ;

}

Высота опорных точек определяется случайным образом, а ландшафт создается функцией C3dShape: :CreateSurtace. Данная функция похожа на C3dShape::CreateRSolid, так что я не стану понапрасну утомлять вас подробностями.

Ландшафт, который вы видите на своем экране, может отличаться от изображенного на Рисунок 4-21. Иногда в изображении вдруг появляются странные пики; это явление обусловлено ошибкой в текущей версии DirectX. Более подробная информация, включающая возможные пути борьбы с пиками, приводится в файле Readme.txt на прилагаемом диске CD-ROM. А пока можно попробовать изменить размер окна, чтобы пик исчез из него.



Построение ландшафта делением граней

От смехотворно простого алгоритма перейдем к более серьезному и посмотрим, как строится поверхность в алгоритме деления граней. На Рисунок 4-22 изображен ландшафт, построенный по новому алгоритму (команда Edit ¦ Landscape 2).

Все грани на Рисунок 4-22 выглядят плоскими и имеют острые края, потому что создавшая их функция пользуется отдельным набором вершин для каждой новой грани. Как мы убедились раньше, в разделе «Создание простых фигур» на стр. 97, для той грани, у которой отсутствуют прилегающие грани, по умолчанию создаются нормали вершин, направления которых совпадают с нормалью к грани. Получившаяся грань выглядит плоской. При желании можно модифицировать программу так, чтобы она генерировала нормали вместе с вершинами, или же немного усложнить код и создавать грани с общими вершинами. Давайте сначала рассмотрим алгоритм, по которому создавалась поверхность на Рисунок 4-22, а затем — исходный текст программы.

Внутри каждой грани поверхности выбирается точка. Я решил определять положение этой точки, усредняя координаты всех вершин грани. Выбрав точку,

/b> ^^?1 Глава 4. Создание фигур

Рисунок. 4-22. Ландшафт, построенный по алгоритму деления гранен



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

. Грань, разделенная на четыре новые грани



После того как будут созданы четыре новые грани, процесс повторяется — четыре новые треугольные грани делятся на новые треугольники, как показано на Рисунок 4-24.

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

Построение ландшафтов

/h2>

Рисунок. 4-22. Грани после повторного деления





void CMainFrame::OnEditInsland2() (

// Создать исходную фигуру, которая является

// простейшей прямоугольной гранью. double х = 10;

double zl = -10;

double z2 = 20;

D3DVECTOR vlist [] = {

(-x, -4, zl},

{-x, -4, z2},

( x, -4, z2},

{ x, -4, zl) };

int nv = sizeof(vlist) / sizeof(D3DVECTOR);

int flist [] = {4, 0, 1, 2, 3,

0 };

NewScene();

m_pShape •= new CSdShape;

m_p3hape-»Create (vlist, nv, flist);

// Делить грани на более мелкие int iCycles = 5;

double dHeight = 1.0;

while (iCycles-) (

// Получить текущий список граней int nPaces = m_pShape-»GetFaceCount () ;

IDirectSDRMMeshBuilder* pMB = m_p3hape-»GetMeshBuilder () ;

ASSERT(pMB) ;

IDirect3DRMFaceArray* pIFA = NULL;

HRESULT hr;

hr = pMB-»GetFaces (SpIFA) ;

ASSERT(SUCCEEDED(hr)) ;

/h2>

Глава 4. Создание (ЬИГУО

// Создать новую фигуру, к которой // будут добавляться новые грани C3dShape* pNewShape = new C3dShape;

// Перебрать грани из списка

for (int iFace = 0; iFace « nFaces; iFace++) {

IDirect3DRMFace* pIFace = NULL;

hr = pIFA-»GetElement (iFace, • SpIFace) ;

ASSERT(SUCCEEDED(hr)) ;

ASSERT(pIFace) ;

// Получить количество вершин DWORD nVert = pIFace-»GetVertexCount () ;

// Разместить буферы D3DVECTOR* pVert = new D3DVECTOR [nVert];;

// Получить данные вершин

hr = pIFace-»GetVertices (&nVert,

pVert, NULL) ;

ASSERT(SUCCEEDED(hr));

ASSERT(pVert) ;

ASSERT(nVert » 2) ;

// Выделить память для новых списков вершин и граней D3DVECTOR* NewVert = new D3DVECTOR [nVert + 1];

int* NewFaceData = new int [4 * nVert + 1] ;

// Скопировать старые вершины //и определить суммы координат C3dVector vNew(0, О, О);

for (DWORD i = 0; i « nVert; i++) (

NewVert [i] =pVert[i];

vNew.x += pVert[i].x;

vNew.y += pVert[i].y;

vNew.z += pVert[i].z;

}

// Вычислить положение новой вершины // на плоскости грани vNew.x /= nVert;

vNew.y /= nVert;

vNew.z /= nVert;

// Прибавить случайное отклонение высоты double dh " dHeight * (1.0 - ((double)(rand() * 100)) / 50.0);

Построение ландшафтов '•'^Щ 115



vNew.y += dh;

// Добавить новую вершину NewVert[nVert] = vNew;

// Создать данные граней int *pfd = NewFaceData;

for (i = 0; i « nVert; i++) (

*pfd++ = 3;

*pfd++ = i;

*pfd++ = (i+1) % nVert;

*pfd++ = nVert; // Новая вершина

} *pfd = 0;

// Включить новые грани в фигуру pNew3hape-»AddFaces (NewVert, nVert+1, NULL, 0, NewFaceData) ;

// Удалить списки вершин и граней delete [] NewVert;

delete [] NewFaceData;

// Удалить данные вершин delete [] pVert;

// Освободить грань pIFace-»Release () ;

}

// Освободить массив граней pIFA-»Release () ;

pMB-»GenerateNormals () ;

// Примечание: не освобождайте интерфейс // построения сеток!!!

// Удалить старую фигуру и сделать текущей новую. delete m_pShape;

m_pShape = pNewShape;

}

// Присоединить итоговую фигуру к макету m pScene-»AddChild(m pShape) ;

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

/b> 1W Глава 4. Создание фигур

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

Создание леса

Давайте попробуем изобразить на экране сценку из жизни леса: в ветвях весело щебечут птички, журчит ручеек, где-то ухает филин. Неплохая мысль — но для начала придется нарисовать что-нибудь, хотя бы отдаленно напоминающее дерево. На Рисунок 4-25 изображена елка (команда Edit ¦АТгее).

Рисунок. 4-25. Елка



Конечно, елки бывают и покрасивее, но пока сойдет и такая. Наше дерево состоит из 25 вершин и 19 граней. Если мы хотим создать целый лес из 100 деревьев (из остальных елок сделали бумагу для книги, которую вы читаете), можно включить в макет еще 99 деревьев, похожих на Рисунок 4-25. Самое время подумать о том, как приказать механизму визуализации нарисовать еще 99 деревьев, в точности аналогичных первому, но расположенных в других местах макета.



Помните, что мы говорили о фреймах и визуальных элементах в главе З? Объект C3dShape содержит фрейм, определяющий его положение, размер, ориентацию и т. д., а также визуальный элемент, который по сути дела описывает набор вершин, граней и т. д. для объекта, который мы хотим увидеть на экране. Функция C3dShape::Clone позволяет включить один и тот же визуальный элемент в несколько разных фреймов. Она создает новый объект C3dShape по существующему объекту, но вместо того, чтобы строить для него новый визуальный элемент, она присоединяет к объекту визуальный элемент исходной фигуры. Таким образом, чтобы изобразить лес, можно создать одно дерево и продублировать его 99 раз. Результат изображен на Рисунок 4-26 (команда Edit ¦ A Forest).

Создание леса

/h2>

Рисунок. 4-26. Лес



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

void CMainFrame::OnEditForest() {

NewScene();

// Создать первое дерево вместе со стволом m_pShape = new C3dShape;

m_pShape-»CreateCone (0, О, О, 1, TRUE, 0, 4, О, О, FALSE) ;

C3dShape trunk;

trunk.CreateRod(0, -2, О, О, О, О, 0.2);

m_pShape-»AddChild(&trunk) ;

// Присоединить дерево к макету m_pScene-»AddChild(m_pShape) ;

// Получить позицию и ориентацию ствола // по отношению к родителю (дереву) C3dVector p, d, u;

trunk.GetPosition(p) ;

trunk.GetDirection(d, и);

// Дублировать дерево 99 раз for (int i = 0; i « 99; i++) {

// Дублировать крону и ствол

/h2>

Глава 4. Создание фигур

C3dShape* pTree == m_pShape-»Clone () ;

C3dShape* pTrunk = trunk.Clone();

pTree-»AddChild(pTrunk) ;

// Задать относительную позицию ствола //по отношению к кроне pTrunk-»SetPosition (p) ;

pTrunk-»SetDirection(d, u) ;

// Присоединить дубли как потомков первого дерева, // чтобы можно было вращать весь лес m_pShape-»AddChild(pTree) ;

// Задать положение нового дерева в макете

pTree-»SetPosition( ( (double) (rand () % 100) / 5)

- 10.0,

0,

(double)(rand() % 100) / 5,



ni_pScene);

// Удалить контейнеры delete pTrunk;

delete pTree;

} )

Непременно обратите внимание на то, что ствол присоединяется в качестве потомка к конусу, образующему крону дерева, поэтому при дублировании кроны со стволом важно, чтобы при присоединении ствола-дубликата к кроне-дубликату была сохранена их относительная позиция. Если этого не сделать, ствол будет развернут в каком-нибудь странном направлении и по всей вероятности вообще не будет соединен с кроной. Попробуйте закомментировать фрагменты позиционирования (pTree-»SetPosition) и ориентации ствола и заново постройте приложение, чтобы посмотреть, что из этого получится.

Второй важный момент заключается в том, что деревья-дубликаты становятся потомками самого первого дерева, чтобы можно было вращать весь лес с помощью одной переменной m_pShape (первое дерево). Поскольку дубликаты являются потомками первого дерева, их позиция должна быть задана по отношению ко всему макету. Если удалить из вызова pTree-»SetPosition необязательный аргумент-фрейм (m_pShape), позиция будет указываться по отношению к родителю, и деревья расположатся неверно. Проверьте!

Довольно о фигурах

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


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