15 мая 2023 года "Исходники.РУ" отмечают своё 23-летие!
Поздравляем всех причастных и неравнодушных с этим событием!
И огромное спасибо всем, кто был и остаётся с нами все эти годы!

Главная Форум Журнал Wiki DRKB Discuz!ML Помощь проекту


Глава 5 Преобразования


В этой главе мы научимся перемещать объекты внутри макета, применяя к ним различные преобразования (называемые также <трансформациями>). Кроме того, мы узнаем, как с помощью определенных преобразований изменять положение, ориентацию и размеры объектов. Ниже рассказывается, как несколько преобразований комбинируются в одном, которое одновременно перемещает и ориентирует объект, а также изменяет его размеры. Правда, интересно? Неприятная сторона заключается в том, что все эти фокусы производятся с помощью матриц. Не пугайтесь: как обычно, мы упрячем всю серьезную математику внутри классов C++. Если вы захотите поближе познакомиться с математическим обоснованием трехмерных преобразований, я бы порекомендовал обратиться к книге by Foley et al. и прочитать главу .

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

Матрицы и преобразования

Большая часть преобразований, которыми мы будем пользоваться, может быть описана в виде вещественной матрицы 3х3. Однако работа с матрицами 3х3 несколько усложняет вычисления, поскольку некоторые преобразования выполняются не так, как другие. Например, перенос реализуется сложением элементов матрицы, а поворот - умножением. Тем не менее, если вставить элементы матрицы 3х3 в матрицу 4х4 и должным образом заполнить свободные места, все преобразования можно будет выполнять посредством операции матричного умножения, поэтому для описания преобразований мы будем пользоваться матрицами 4х4. Если вы незнакомы с однородной системой координат, которая применяется при описании преобразований в трехмерном пространстве (а кто с ней знаком, кроме математиков?), вы наверняка чувствуете себя сбитым с толку. Давайте немедленно решим эту проблему и начнем с небольшого примера того, как пользоваться матрицами для преобразования координат точки в пространстве. Поскольку формулы для трехмерного случая оказываются довольно длинными, мы вместо этого рассмотрим упрощенный пример на плоскости - уверяю вас, в трехмерном пространстве он работает точно так же.

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

х

У 1

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

WlaTDHLlbl и поеобпазпняния "it!!R 121


1 0 dx О 1 dy 00 1

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

"1 0 dx] Гх] Г1 * х + 0 - у + 1 * dx] Гх + dx" 01dyxy=0*x+l*y+l*dy=y+dy 001 1 0 * х + 0 * у + 1  1 1

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

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

C3dMatrix::C3dMatrix()

{

т_00=1.0; т_01њ0.0; т_02=0.0; т_03=0.0;

т_10=0.0; т_11=1.0; т_12=0.0; т_13=0.0;

т_20=0.0; т_21=0.0; т_22=1.0; т_23=0.0;

т_30=0.0; т_31=0.0; т_32=0.0; т_33=1.0;

}

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

C3dVector v (х, у, z) ;

C3dMatrix т;

т.Translate(dx, dy, dz);

v = т * v;

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

122

Глава 5. Поеобоазования


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

Не следует полагать, будто функция C3dMatrix: translate просто инициализирует матрицу; как нетрудно убедиться по приведенному ниже фрагменту, на самом деле она комбинирует преобразование переноса с любыми преобразованиями, уже занесенными в матрицу:

void C3dMatrix::Translate(double dx, double dy, double dz)

{

C3dMatrix tx( 1, О, О, О,

О, 1, О, О, О, 0, 1, О, dx, dy, dz,l);

*this *= tx;

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

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

mFinal = ml * m2 * m3;

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

Теперь давайте посмотрим, как матричные преобразования используются на практике, при работе с трехмерными объектами. Чтобы применить матрицу преобразования к объекту C3dShape, вовсе не нужно заниматься умножением. Вместо этого следует скомбинировать новое преобразование с текущим, хранящимся во фрейме объекта. Вспомните - фрейм, определяющий положение и ориентацию объекта в макете, на самом деле представляет собой преобразование, применяемое ко всем точкам фигуры. Кроме того, фрейм объекта является потомком другого фрейма, расположенного выше в иерархии, и для определения окончательного положения объекта необходимо скомбинировать результаты всех преобразований в иерархии фреймов. Мы собираемся изменить преобразование, хранящееся во фрейме объекта, расположенном где-то внизу иерархии фреймов, пример которой изображен на рис. 5-1.

Изображенный на рис. 5-1 сложный объект состоит из двух фигур, каждая из которых обладает собственным фреймом и визуальным элементом. Для преобразования всего объекта следует модифицировать объединяющий фрейм, который является общим родителем для фреймов обоих компонентов.

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

Поеобоазования тоехмеоных объектов vis' 123


82.jpg

Рис. 5-1. Иерархия фреймов

каждом конкретном случае? Я надеюсь, что после знакомства с примерами вы и сами найдете ответ на этот вопрос.

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

83.jpg

Рис. 5-2. Окно приложения до применения преобразований

124

Глава 5. Преобразования


Перенос

Первый тип рассматриваемых нами преобразований - перенос. Переносом называется простое прямолинейное перемещение объекта в одном направлении. Для переноса объекта следует прибавить к его координатам х, у и z величины смещений. На рис. 5-3 изображен результат переноса по оси х.

84.jpg

Рис. 5-3. Перенос по оси х

Фрагмент программы, в котором был осуществлен этот перенос, выглядит следующим образом:

void CMainFrame::OnEditTranslatex() (

if (!m_pCurShape) return;

C3dMatrix m;

m.Translate (2, О, О);

m_pCurShape->AddTransform(m, D3DRMCOMBINE_AFTER) ;

}

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

Поворот

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

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

125


85.jpg

Рис. 5-4, Поворот вокруг оси у

Ниже приведен текст функции, в которой выполняется поворот:

void CMainFrame::OnEditRotatey() {

if (!m_pCurShape) return;

C3dMatrix m;

m.RotatefO, 45, 0);

m_pCurShape->AddTransform(m, D3DRMCOMBINE AFTER);

Масштабирование

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

Наш самолет выглядит по меньшей мере странно! Результат последующего применения аналогичного масштабирования по оси z изображен на рис. 5-6.

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

void CMainFrame::OnEditscalex() {

if (!m_pCurShape) return;

C3dMatrix m;

m.Scale(2.0, 1.0, 1.0);

m_pCurShape->AddTransform(m, D3DRMCOMBINE AFTER);

}

126

Глава 5. Преобразования


86.jpg

Рис. 5-5. Масштабирование объекта по осям х и у

87.jpg

Рис. 5-6. Объект после равномерного масштабирования по осям х, у и z

Обратите внимание на то, что коэффициенты масштабирования по осям у и z равны 1, а не 0. Если присвоить им нулевые значения, вам будет нелегко рассмотреть свои объект!

117


Порядок преобразований

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

88.jpg

Рис. 5-7. Перенос вдоль оси х, за которым следует поворот вокруг оси у

Совпадает ли такой результат с тем, что вы ожидали увидеть? Текст функции приведен ниже:

void CMainFrame::OnEditTranrot() {

if (!m_pCurShape) return;

C3dMatrix m;

m.Translate(3, 0, 0) ;

m.Rotate(0, 45, 0) ;

m_pCurShape->AddTransform(m, D3DRMCOMBINE_AFTER) ;

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

Давайте повторим те же самые преобразования, но на этот раз изменим их порядок. Результат изображен на рис. 5-8.

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

128

Глава 5. Поеобоазования


89.jpg

Рис. 5-8. Поворот вокруг оси у, за которым следует перенос вдоль оси х

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

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

В нашем приложении имеются команды меню, поворачивающие объект вокруг осей макета, по аналогии с фрагментом на стр. 128. Сюда также включены команды для выполнения поворотов вокруг собственной оси объекта. Ниже приводится пример поворота вокруг оси у объекта:

void CMainFrame::OnEditRobjy() {

if (!m_pCurShape) return;

C3dMatrix m;

m.Rotate(0, 45, 0) ;

m_pCurShape->AddTransform(m, D3DRMCOMBINE_BEFORE) ;

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

Возвращение на базу

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

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

129


и возвращает ему исходное положение и ориентацию. Ниже приведена соответствующая функция:

void CMainFrame::OnEditReset()

{

if (!m_pCurShape) return;

C3dMatrix m;

m_pCurShape->AddTransform(m, D3DRMCOMBINE_REPLACE) ;

}

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

Экспериментируйте!

В приложении имеется окно диалога, открываемое командой Edit Ѓ Transform Shape. Оно используется для задания произвольных преобразований переноса, поворота и масштабирования. В любом случае можно указать, следует ли применять новое преобразование до текущего, после него или же заменить текущее преобразование новым. Несколько опытов с окном диалога Transforms, изображенным на рис. 5-9, заполнят все возможные пробелы в вашем понимании того, как же комбинируются преобразования.

810.jpg

Рис. 5-9. Окно диалога Transforms

Житейские мелочи

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

130

Глава 5. Преобразования


колоды по-прежнему оставались прямыми, но не были перпендикулярны столу, как показано на рис. 5-10.

811.jpg

Рис. 5-10. <Сдвинутая> колода карт

Итак, мы применили к колоде преобразование сдвига. На рис. 5-11 показано, что получится в результате применения сдвига к нашему самолету.

812.jpg

Рис. 5-П. Результат применения сдвига

Чтобы вам было легче разглядеть самолет после сдвига, я немного увеличил его, применив перед сдвигом масштабирование с одинаковыми коэффициентами по всем трем осям. Я не смог придумать для сдвига достойного применения в трехмерном приложении, но наверняка вы сможете это сделать, поэтому я привожу текст функции, выполнившей преобразование сдвига на рис. 5-11:

void CMainFrame::OnEditShear() (

if (!m_pCurShape) return;

Til

Далее..