Перемещение объектов
Глава 6 Перемещение объектов
в макете
Устройства ввода
По своему опыту могу сказать, что при написании приложений для Microsoft Windows на пользовательский интерфейс обычно уходит больше времени, чем на все основное содержание программы. Думаю, при работе над любым продуктом хотя бы с удовлетворительным пользовательским интерфейсом на последний тратится около 75 процентов всего времени. Честно говоря, в своих приложениях я не вкладываю в него таких усилий (те читатели, которые имели несчастье с ними работать, в этом наверняка убедились).
Пожалуй, тщательнее всего я реализовываю интерфейс приложений, которые я пишу для своих детей. Они (приложения, а не дети) должны быть как можно проще. Например, однажды я написал для своего полуторагодовалого сына Марка простейший графический редактор, в котором пользовательский интерфейс почти полностью отсутствовал. В нем не было ни меню, ни кнопок, ни управляющих клавиш, и при работе с ним можно было даже обойтись без нажатия кнопок мыши. Тем не менее его инструменты позволяли нарисовать вполне нормальную картинку.
На прошлой неделе Марку исполнилось три года, и он начал проявлять интерес к ракетам, и потому я попробовал изобразить ракеты на своем компьютере. Приступая к работе над приложениями для этой книги, я обдумывал множество идей для игр, которые бы понравились Марку. Но вскоре стало очевидно, что клавиатура и мышь не могут обеспечить интерфейс для трехмерного приложения, который подошел бы трехлетнему ребенку. Честно говоря, мои первые эксперименты с перемещением трехмерных объектов не очень обрадовали даже меня самого. Проблема заключалась в том, что я пытался построить интерфейс на основе мыши и нескольких клавиш. Конечно, большинству читателей приходилось играть в трехмерные игры, которые имели вполне нормальный интерфейс на базе мыши и клавиатуры. Однако мой сын решительно не мог удерживать нажатой левую кнопку мыши вместе с клавишей Shift, да еще притопывать правой ногой от нетерпения.
На сцене появляется джойстик. Много лет назад, когда компьютеры приводились в действие энергией пара, а процессор 80286 казался чудом техники, я написал исходный драйвер джойстика для мультимедиа-расширения Microsoft Windows (кстати говоря, довольно сомнительная заслуга). Впрочем, все это было давно, и с тех пор я прочно забыл о существовании джойстиков, пока несколько недель назад не зашел в магазин Microsoft. Именно в этот момент я решил, что мои трехмерные приложения будут работать под управлением джойстика, и купил
/b> ¦¦ у Глава 6. Перемещение объектов в макете
Microsoft SideWinder Pro. Довольно быстро выяснилось, что в трехмерных приложениях джойстик действительно удобнее мыши. SideWinder обладает четырьмя параметрами, генерирующими данные для приложения: координатами Х и Y, угловой и линейной скоростью. С помощью кнопок на боковой поверхности джойстика я мог вращать объект вокруг трех осей и управлять его положением.
Немного позднее мне подарили манипулятор SpaceBall Avenger — весьма эффектное устройство, выпущенное компанией Spacetec IMC Corp. Он обладает шестью степенями свободы, то есть позволяет получить выходные параметры для смещения по осям х, у и z, а также для вращательного движения вокруг осей r, u и v. В конструкции манипулятора использованы датчики давления, которые с высокой чувствительностью реагируют на все толчки и повороты шарика.
Итак, у меня появились два джойстика, которые работали по-разному, а программа поддерживала только один из них. Я решил создать обобщенную модель устройства ввода, которую можно было бы приспособить к любому конкретному устройству и настроить его так, как мне хочется. Поначалу казалось, что задача не из сложных — во всяком случае, до тех пор, пока я не взялся за нее. Результат моих стараний не назовешь шедевром, однако он вполне пригоден для практического использования — если у вас есть 10—15 свободных минут, вы сможете настроить свой джойстик самым немыслимым образом. После того как работа была завершена, я подумал, что она по крайней мере послужит примером того, как можно построить обобщенную модель устройства ввода. Я бы не советовал распространять ее в коммерческих целях, если вы дорожите своей репутацией.
Время, потраченное на настройку джойстика, вознаграждается сторицей: мой сын обожает гонять объекты по экрану, выкручивая рукоять джойстика или беспорядочно толкая SpaceBall во всех направлениях.
Для полноценной работы с приложением из этой и последующих глав я бы порекомендовал купить джойстик — общаться с ним гораздо удобнее, чем с мышью.
Модель устройства ввода
На Рисунок 6- 1 изображена модель устройства ввода* с точки зрения приложения. Когда приложению требуется выполнить цикл обновления, оно вызывает функцию Update объекта, управляющего процессом ввода (контроллера ввода). Контроллер опрашивает устройство ввода и получает обновленную информацию о его аппаратном состоянии. Узнав ее, контроллер изменяет положение и ориентацию фрейма, к которому он присоединен в приложении, а затем уведомляет приложение о причине обновления состояния. Все эти действия обычно совершаются в периоды пассивности приложения. Помимо событий, изображенных на Рисунок 6-1, возможны и другие. В частности, любые сообщения от клавиатуры или мыши, полученные окном, в котором воспроизводится трехмерное изображение, передаются контроллеру и устройству ввода. Это делается для того, чтобы получать входные данные от мыши или клавиатуры без обязательного опроса этих устройств.
• Обратите внимание на то, чти устройством ввода (input device) в данном случае автор называет не физическое устройство (клавиатура, мышь, джойстик), а класс C++, назначением которого является генерация входных данных для программы. — Примеч. персе.
Модель устройства ввода ^Щ' 135
Рисунок. 6-1. Рабочий цикл устройства ввода
Устройство ввода
Назначение устройства ввода состоит в том, чтобы получить данные от аппаратуры и сгенерировать по ним шесть величин: х, у, z, r, u и v. Значения х, у и z представляют собой линейные смещения, а r, u и v — угловые скорости. Оси устройства ввода не связаны с осями макета или конкретного объекта; это не более чем необработанные входные данные, которые используются контроллером для смены положения объекта. На Рисунок 6-2 показаны общие зависимости между осями и угловыми скоростями.
Разумеется, при работе с таким устройством, как SpaceBall, значения х, у, z, г, u и v генерируются просто — достаточно опросить аппаратуру, применить некоторый масштабный множитель и вернуть результат. Для физических устройств, не способных генерировать данные по шести осям, устройство ввода должно получить аппаратные данные и обработать их так, чтобы создать выходные значения для всех шести параметров. Например, устройство ввода из библиотеки 3dPlus, работающее с мышью, получает координаты х и у курсора мыши и в зависимости от состояния клавиш Shift и Ctrl определяет, какие выходные значения следует изменить. Таким образом, если во время перемещения мыши удерживать левую кнопку и не нажимать никаких клавиш, изменяются координаты х и у. При нажатой клавише Shift входное значение координаты х переходит в угловую скорость v, а координата у—в координату z.
/b>
Глава 6. Перемещение объектов в макете
Рисунок. 6-2. Взаимосвязь между значениями r, u, v и х, у, z
Библиотека 3dPlus включает поддержку трех различных устройств ввода: клавиатуры, мыши и джойстика. Каждое устройство реализовано в виде класса C++, производного от C3dlnputDevice.
Устройство ввода с клавиатуры
Устройство ввода с клавиатуры обрабатывает сообщения WM_KEYDOWN, посылаемые ему контроллером. Сообщения клавиатуры используются для увеличения или уменьшения текущих значений параметров x,y,z,r,unv.B табл. 6-1 показано, как различные комбинации клавиш влияют на значения выходных параметров.
Таблица 6-1. Управляющие функции клавиатуры | |||
Клавиша | Normal | Shift | Ctrl |
Left arrow | X- | V- | u- |
Right arrow | X++ | V++ | U++ |
Up arrow | Y++ | Z++ | R++ |
Down arrow | Y- | z- | R- |
Знак «+» на цифровой клавиатуре | Z- | ||
Знак «-» на цифровой клавиатуре | Z++ | ||
Page Up | V++ | ||
Page Down | V- | ||
Home | U++ | ||
End | u- | ||
Insert | R++ | ||
Delete | R- |
Функция, управляющая работой устройства ввода с клавиатуры, представляет собой оператор switch, в котором обрабатываются сообщения от различных клавиш. Ниже приведена первая часть функции из файла SdlnpDev.cpp каталога Source библиотеки 3dPlus, обрабатывающая нажатия клавиш <— и —>:
void C3dKeyInDev::OnKeyDown(HINT nChar, UINT nRepCnt,
UINT nFlags)
(
double dine = 0.02;
switch (nChar) { case VKJ3HIFT:
in_bShift = TROE;
break;
case VK_CONTROL:
m_bControl = TRUE;
break;
case VK_RIGHT:
if (m_b3hift) { Inc(m_st.dV) ;
} else if (m_bControl) { Inc(m_st.dU) ;
) else (
Inc(m_st.dX) ;
} break;
case VK_LEFT:
if (m_bShift) { Dec(m_st.dV) ;
} else if (m_bControl) { Dec(m_st.dU) ;
) else (
Dec(m_st.dX) ;
} break;
Устройство ввода от мыши
Устройство ввода от мыши выглядит несколько проще. Поскольку мышь обладает только двумя степенями свободы, необходимо определить, каким образом два входных параметра отображаются на шесть выходных осей (табл. 6-2).
Таблица 6-2. Управляющие функции мыши
Входной параметр Normal Shift Ctrl
X X -V -U
Y -Y -Z -R
/b> iiisi^ Глава б. Пепемешение объектов в макете
Обратите внимание на то, что некоторые параметры инвертируются. Я изменил направление осей, чтобы управление стало более логичным. Код устройства ввода от мыши состоит из двух функций: C3dMouselnDev::OnUserEvent и C3dMouselnDev::GetState. Первая функция, исходный текст которой приведен ниже, находится в файле 3dlnpDev.cpp. Данная функция обрабатывает перемещение мыши и захватывает ее указатель (то есть ограничивает его перемещение текущим окном) при нажатии левой кнопки:
void C3dMouseInDev::OnUserEvent(HWND hWnd, UINT uiMsg,
WPARAM wParam, LPARAM IParam) (
switch (uiMsg) { case WM_LBUTTONDOWN:
::SetCapture(hWnd) ;
m_bCaptured = TRUE;
break;
case WM_LBUTTONUP:
if (m_bCaptured) { ::ReleaseCapture () ;
m_bCaptured = FALSE;
} break;
case WM_MOUSEMOVE:
if (m_bCaptured) (
// Внимание: экранные координаты! (см. C3dWnd)
m_ptCur.x = LOWORD(IParam);
m_ptCur.y = HIWORD(IParam);
m_dwFlags = wParam;
} break;
default:
break;
}
\ i
Положение мыши запоминается в m_ptCur, локальной структуре класса CPoint. Вторая функция, исходный текст которой приведен ниже, вызывается, когда контроллер запрашивает текущее состояние устройства ввода:
BOOL C3dMouseInDev::GetState(_3DINPUTSTATE& st) {
if (m_ptPrev.x « 0) { m_ptPrev = m_ptCur;
}
Устоойство ввода '"SH 139
if (m_dwFlags & MK_SHIFT) {
m_st.dz = -d;
} else if (m_dwFlags & MK_CONTROL) {
rri_st.dR = -d;
} else (
m_st.dY = -d;
) )
m_ptPrev = m_ptCur;
st = m_st;
return TRUE;
Функция обрабатывает полученные значения х и у таким образом, чтобы обеспечить небольшую «мертвую зону» для малых смещений и предотвратить случайное перемещение объекта. Затем смещения умножаются на коэффициент пропорциональности, чтобы перемещение объектов всегда происходило в правильном масштабе. Наконец, в зависимости от текущего состояния клавиш Shift и Ctrl функция определяет, какие выходные параметры следует изменить.
Устройство ввода от джойстика
Устройство ввода от джойстика реализуется несколько сложнее, чем ввод от мыши или клавиатуры. На Рисунок 6-3 изображено окно диалога Joystick Settings.
Рисунок. 6-3. Окно диалога Joystick Settings
Значение каждого выходного параметра может определяться по любой из входных осей, а кнопка джойстика может выступать в роли модификатора. Например, из Рисунок 6-3 видно, что значение выходного параметра v определяется значением входного параметра х, но только при нажатой кнопке 4. В столбцах Value изображены текущие значения параметров. Левый столбец показывает те-
Угтплмгтял капля
141
кущее входное значение, полученное от джойстика; темно-серая полоса соответствует «мертвой зоне». Если входное значение лежит внутри «мертвой зоны», выходное значение не изменяется. Наличие «мертвой зоны» позволяет предотвратить мелкие смещения объектов в тех случаях, когда отпущенная рукоять джойстика не возвращается точно к нейтральному положению. Правый столбец Value изображает выходное значение параметра.
Кроме того, вы можете изменить масштабы осей. Увеличение числа в столбце Scale соответствует повышению чувствительности джойстика, причем отрицательные значения меняют направление оси на противоположное. Конфигурация, показанная на рисунке, была выбрана мной для джойстика Microsoft SideWinder. При работе со SpaceBall остается лишь задать коэффициент пропорциональности между параметрами (х отображается на х, у — на у и т. д.). На Рисунок 6-4 изображен типичный график зависимости выходных значений параметров от входных. Плоский участок в центре соответствует «мертвой зоне».
Рисунок* 6-4» Типичный график зависимости вход/выход для джойстика
Конфигурация джойстика сохраняется в системном реестре. Я решил создавать отдельный вариант конфигурации для каждого типа джойстика и для каждого приложения. Если у вас имеются несколько джойстиков, то при смене активного джойстика можно обойтись без повторной конфигурации. Не исключено, что идея сохранения отдельной конфигурации для каждого приложения покажется довольно странной, но я обнаружил, что в некоторых приложениях желательно настроить джойстик нестандартным образом. Данные конфигурации хранятся в реестре с ключом:
HKEY_CURRENT_USER\Software\3dPlus\<<MMH-npKnoiKeHMA>>\Sett.i.i-igs \Joystick\«тип-джойстика»
Большой объем кода для работы с джойстиком не позволяет привести его в книге, поэтому я предлагаю вам просмотреть файл 3dJoyDev.cpp в каталоге Source библиотеки 3dPlus.
Гпаоа R Попоклашаииа /^Дт-ои-г^о о идилата
Контроллер ввода
Задача контроллера ввода заключается в том, чтобы получить от устройства ввода значения параметров х, у, z, r, u и v и определенным образом применить их к объекту CSdFrame. Я создал два различных типа контроллеров ввода: позиционный контроллер (position controller) и контроллер полета (flying controller). Контроллеры обоих типов могут использоваться для манипуляций с объектами макета или с камерой. Контроллеру необходимо указать фрейм, с которым он должен работать, а остальное происходит автоматически. Помимо перемещения объекта, контроллер уведомляет приложение о различных событиях — скажем, об изменении параметра х или о нажатии определенной кнопки, — на которые приложение должно реагировать определенным образом. На Рисунок 6-5 изображено окно диалога, которое вызывается из меню Edit приложения Moving. Здесь можно выбрать разновидность контролируемого объекта, тип контроллера и устройство ввода.
Рисунок. 6-5. Окно диалога Control Device
Позиционный контроллер используется для перемещения объектов внутри макета. Параметры х, у и z определяют положение объекта по отношению к фрейму макета; следовательно, при смещении джойстика влево объект перемещается вдоль оси х макета. Повороты происходят относительно оси объекта, а не начала координат, и это выглядит достаточно разумно. Например, при повороте джойстика объект вращается, а его центр тяжести остается на месте. Любой пользователь может достаточно быстро научиться работать с таким контроллером и перемещать объекты внутри макета.
Позиционный контроллер также может использоваться для перемещения камеры, что, в сущности, равносильно перемещению всего макета. Тем не менее результаты такого перемещения иногда выглядят довольно странно, поскольку все движения осуществляются по отношению к макету, а не к камере; если развернуть камеру вокруг оси у на 90 градусов, чтобы она была обращена влево, любое движение вперед будет восприниматься как движение вправо.
Контроллер полета используется для имитации «полета» объекта или камеры внутри макета. Параметры х и у служат для определения углов атаки и крена,
z определяет скорость, а и — угол тонгажа. Идея состоит в том, чтобы привести объект в прямолинейное движение и затем выбирать его траекторию посредством изменения углов атаки, крена и тонгажа. В исходном варианте программы углы крена и атаки умножались на скорость, чтобы имитация получалась более реалистичной. Однако вскоре выяснилось, что пилота из меня не выйдет, поэтому я пошел по более простому пути и допустил изменение ориентации даже для неподвижного объекта. Если вам это покажется нелогичным, попробуйте поработать с текущим вариантом и затем модифицировать его так, чтобы учитывать скорость полета. Что же именно модифицировать, спросите вы? Приведенную ниже функцию, которая находится в файле 3dlnCtlr:
void C3dFlyCtlr::OnUpdate(_3DINPUTSTATE& st,
C3dFrame* pFrame) t
// Определить скорость (по значению параметра z)
double v = st.dZ / 10;
// Получить углы атаки, крена и тонгажа // для осей х, у и и double pitch = st.dY / 3;
double roll = -st.dX / 3;
double yaw = 5t.dU / 5;
// Умножить угол атаки и крена на скорость // для повышения реализма // pitch *= v;
// roll *= v;
pFrame-»AddRotation(l, 0, 0, pitch, D3DRMCOMBINE_BEFORE) ;
pFrame-»AddRotation(0, 0, 1, roll, D3DRMCOMBINE BEFORE);
pPrame-»AddRotation(0, 1, 0, yaw, D3DRMCOMBINE^BEFORE) ;
// Получить вектор текущего направления double xl, yl, zl;
pFrame-»GetDirection (xl, -yl, zl);
// Умножить вектор направления на скорость xl *= v;
yl *= v;
zl *= v;
// Определить текущее положение double х, у, z;
pFrame-»GetPosition (х, у, z);
// Обновить текущее положение х += xl;
У += yl;
z += zl;
/b> ^Р? Глава 6. Перемещение объектов в макете
pFrame-»SetPosition (x, y, z
Функция C3dFlyCtrl::OnUpdate изменяет положение и ориентацию фрейма перемещаемого объекта на основании данных, полученных от устройства ввода. Эта функция вызывается каждый раз, когда требуется обновить положение объекта. Ее аргументами являются описание текущего состояния входного устройства (значения его параметров) и указатель на фрейм, с которым она должна работать. Из всего кода контроллеров и устройств ввода данная функция представляет наибольший интерес, поэтому мы подробно рассмотрим ее.
Начнем со структуры для хранения данных, полученных от джойстика:
typedef struct 3DINPUTSTATE {
double dX // -1«= значение «=1
double dY // -1«= значение «=1
double dZ // -1«= значение «=1
double dR // -1«= значение «=1
double dU // -1«= значение «=1
double dV // -1«= значение «=1
double dpov; // 0 «= значение «=359
// (значения «О являются недопустимыми) DWORD dwButtons;// I = кнопка активна (нажата) ) _3DINPUTSTATE;
Как видно из листинга, значения шести основных параметров лежат в интервале от -1,0 до 1,0. Кроме того, в структуре присутствует член dPov, определяющий направление, в котором вы смотрите, — вперед, влево, вправо и т. д. (на некоторых джойстиках имеется специальная кнопка для выбора направления). Значение dPov представляет собой угол в градусах, измеряемый от направления «вперед».
Контроллер задает скорость и углы крена, атаки и тонгажа по входным значениям параметров z, x, у и и соответственно. Для повышения чувствительности я применил масштабные коэффициенты, значения которых были определены эмпирическим путем.
Если вы захотите усложнить управление летящим объектом, попробуйте убрать комментарии из строк, в которых углы атаки и крена умножаются на скорость.
После определения текущей скорости и величины смещения объекта, следующим шагом является применение поворотов к фрейму. Для этого мы вызываем функцию AddRotation и указываем, что поворот должен быть выполнен до текущего преобразования. Это необходимо для того, чтобы объект вращался вокруг собственной оси, а не вокруг оси макета.
Завершающий шаг — перемещение объекта в новое положение. Сначала мы вызываем функцию GetDirection, чтобы получить вектор направления объекта (то есть направление, в котором он летит). Затем вектор направления умножается на скорость — их произведение равно величине смещения от текущего положения объекта. Наконец, мы определяем текущее положение объекта, прибавляем к нему смещение и переносим объект в новое положение.
Возможно, вы обратили внимание на то, что в программе используются версии функций GetDirection, GetPosition и SetPosition, в которых значения x, у и z
Устройство ввода
/h2>
заданы в виде отдельных аргументов, а не в виде объекта C3dVector. Никакой особой причины для этого нет, и вы вполне можете в качестве упражнения переписать данные функции с использованием аргументов-векторов.
Последнее замечание: скорость перемещения в нашем случае не является постоянной. Функции обновления вызываются в периоды пассивной работы приложения, а количество времени, которое требуется для перерисовки макета, зависит от взаимного расположения объектов. Если вы захотите добиться постоянной скорости перемещения, придется пойти более сложным путем — например, измерять текущее время функцией timeGetTime (объявленной в файле Mmsystem.h) и определять смещение каждого объекта в зависимости от времени.
Самостоятельно движущиеся объекты
Контроллер полета способен привести объект в движение, однако все равно вам приходится непрерывно управлять его перемещением. Лично я не люблю подолгу сидеть за рулем, поэтому разработал несколько примеров того, как заставить объекты самостоятельно двигаться по фиксированной траектории.
Первый пример, находящийся в каталоге Cruise, приводит в движение камеру, чтобы создать иллюзию кругового полета на самолете вокруг горы. На Рисунок 6-6 изображен внешний вид экрана приложения (кроме того, на вкладке имеется цветная иллюстрация).
Рисунок. 6-6. Полет над холмами
Перед тем как писать это приложение, я нарисовал план местности на миллиметровке, обозначив на нем границы всех холмов. Затем я вручную закодировал все вершины и данные граней для создания ландшафта. Не стану приводить здесь фрагмент исходного текста, поскольку он состоит лишь из длинного массива вершин, за которыми следует не менее длинный список данных граней. При желании можете найти его в файле MainFrm.cpp в каталоге Cruise.
/h2>
Глава 6. Перемещение объектов в макете
Кроме того, я вручную изобразил небольшой самолет и наложил на ландшафт текстуру, чтобы придать ему более привлекательный вид (создание и применение текстур рассмотрено в главе 8). Последним этапом в создании макета стала установка камеры в начальной позиции и увеличение ее обзорного поля для получения более широкой панорамы:
BOOL CMainFrame::SetScene() t
// Задать траекторию полета камеры m_vCamera = C3dVector(5, 5, 0);
m_dRadius = 5.0;
// Задать обзорное поле m_pScene-»SetCameraField (1.5) ;
}
Траектория выбирается таким образом, чтобы камера вращалась вокруг заданной точки. На Рисунок 6-7 изображено движение камеры в нашем макете.
Рисунок. 6-7. Траектория полета камеры
Все, что осталось сделать, — организовать совместное перемещение камеры и самолета при каждой итерации:
BOOL CMainFrame::Update(double d)
(
// Обновить положение камеры C3dMatrix r;
r.Rotate(0, 2.0, 0) ;
m vCamera = r * m vCamera;
m_pScene-»SetCameraPosition (m_vCamera) ;
// Задать верхний вектор C3dVector vu(0, 1, 0) ;
// Построить вектор направления C3dVector vf = m_vCamera * vu;
m_p3cene-»SetCameraDirection (vf, vu) ;
// Задать положение самолета относительно камеры r.Rotate(0, 20, 0);
C3dVector vp = r * m vCamera;
m_pPlane-»SetPosition(vp) ;
// Задать направление C3dMatrix rp;
rp.Rotate(0, 0, 10); // Слегка покачаем крьшьями vu = rp * vu;
vf = vp * vu;
m_pPlane-»SetDirection (vf, vu) ;
return m_wnd3d.Update(TRUE);
)
Текущее положение камеры хранится в объекте C3dVector. Для определения ее нового положения вектор умножается на матрицу поворота. Затем камера переносится на новое место — но это еще не все. Необходимо изменить ориентацию камеры, чтобы она по-прежнему была направлена по касательной к окружности. Чтобы вычислить новое направление камеры, мы умножаем (векторно) верхний вектор (vu) на вектор положения камеры (m_vCamera). Вектор-результат совпадает с вектором направления камеры (Рисунок 6-8).
Вычисление вектора направления камеры 148 SUSy Глава 6. Пеоемешение объеет-ов в мякртр
Векторное произведение оказывается очень полезным, когда требуется найти вектор, перпендикулярный плоскости, которая определена двумя другими векторами, как в нашем случае.
Последнее, что осталось сделать, — вычислить положение и направление маленького самолета, летящего перед камерой. Самолет перемещается по той же траектории, что и камера, однако он на несколько градусов опережает камеру. Чтобы определить положение самолета, мы берем вектор положения камеры и поворачиваем его чуть дальше, пользуясь для этого другой матрицей (г). При определении ориентации самолета я сначала вычислял векторное произведение точно так же, как и для камеры. Однако мне показалось, что смотреть на самолет, который идеально ровно летит впереди камеры, довольно скучно. Я слегка повернул вектор вверх, чтобы создать иллюзию покачивания самолета. Эффект не очень впечатляющий, но зато легко реализуемый.
Относительное движение
Наше следующее приложение имитирует часовой механизм, состоящий из нескольких частей, которые находятся в непрерывном движении по отношению друг к другу. На Рисунок 6-9 изображен внешний вид окна приложения, находящегося в каталоге Clock (на вкладке имеется цветная иллюстрация).
Рисунок. 6-9. Часовой механизм
Механизм состоит из трех вращающихся стержней. К центральному стержню прикреплена минутная стрелка и небольшая шестеренка. Ко второму стержню, охватывающему часть центрального, прикреплена часовая стрелка и большая шестеренка. На третьем стержне имеются две шестеренки, большая и маленькая, которые сцеплены с шестеренками для двух стрелок — минутной (центральный стержень) и часовой (внешний стержень).
/h2>
Относительное движение
Когда я впервые попытался создать это приложение, то изобразил шестеренки в виде дисков, и на «все про все» у меня ушло около часа. Отладка работы шестеренок потребовала уже целых трех часов! Фрагмент кода, в котором конструируется данный механизм, выглядит довольно просто. Сначала мы создаем стержни и присоединяем к ним шестеренки и стрелки в качестве фреймов-потомков. Затем начинаем вращать стержни. Я подобрал частоту вращения таким образом, чтобы создать впечатление, будто механизм действительно приводится в действие шестеренками. Если внимательно присмотреться, можно заметить, что иллюзия получилась не полной.
Чтобы весь часовой механизм вращался в окне, я создал фрейм, являющийся родительским по отношению ко всем трем стержням. Когда этот фрейм поворачивается вокруг оси у, изображение работающего механизма также начинает вращаться (наблюдательные читатели могли заметить, что передаточный коэффициент шестеренок составляет 4:1 вместо более привычного 12:1, как в большинстве часов). Давайте рассмотрим фрагмент файла MainFrm.cpp, в котором создается стержень с минутной стрелкой. Два других стержня определяются аналогичным образом:
BOOL CMainFrame::SetScene() (
// Создать часовой механизм C3dFrame clock;
clock.Create(m_pScene) ;
double dSpin = -0.1;
// Создать стержень с минутной стрелкой C3dFrame si;
si.Create(Sclock) ;
C3dShape rl;
rl.CreateRod(0, 0, -0.5, О, О, 10, 0.4, 16);
rl.SetColor(0, 0, 1);
sl.AddChild(Srl) ;
// Присоединить минутную стрелку CHand bighand(lO) ;
sl.AddChild(Sbighand) ;
bighand.SetPosition(0, 0, 0) ;
// Присоединить шестеренку CGear gl(1.5, 1.5, 8) ;
sl.AddChildf&gl) ;
gl.SetPosition(0, 0, 5.5);
// Привести стержень во вращение sl.SetRotation(0, 0, 1, dSpin);
}
/b> Я»:'?' Глава 6. Перемещение объектов в макете
Фрейм стержня создается как потомок по отношению к фрейму всего механизма. Затем к фрейму стержня присоединяется цилиндрический объект, который является его визуальным представлением. Минутная стрелка создается как объект класса CHand, производного от CSdShape, который мы рассмотрим чуть позже. Шестеренка тоже является объектом отдельного класса CGear, производного от CSdShape, и точно так же присоединяется к фрейму стержня. Последнее, что осталось сделать, — привести фрейм во вращение функцией C3dFrame::SetRotation.
Стрелки создаются из двух цилиндров и конуса:
CHand::CHand(double 1)
{
CreateRod(0, 0, О, О, О, 0.5, 1, 16);
SetColor(l, 1, 0);
CSdShape r;
r.CreateRod(0, 0, 0.25, 0, 1-3, 0.25, 0.20, 16);
r.SetColor(0, 0, 1);
AddChild(&r) ;
CSdShape с;
c.CreateCone(0, 1-3, 0.25, 0.75, TRUE, 0, 1, 0.25, 0, FALSE, 16);
c.SetColor(l, 1, 0);
AddChild(Sc) ;
}
С шестеренками дело обстоит несколько сложнее. Внешний и внутренний радиус зубцов определяется двумя окружностями. Затем окружности разбиваются на части по числу зубцов, что и дает нам положения вершин (Рисунок 6-10). Генерация списка данных для внешних граней зубцов завершает первую стадию создания фигуры.
Рисунок. 6-10. Конструирование зубцов шестеренки
При создании боковых граней шестеренки используются нормали, чтобы грани воспроизводились в виде плоских поверхностей. Если не задавать нормали, механизм визуализации скругляет стороны шестеренок, и на их гранях появляют-
Относитепьное лвижймий Ш^ 151
ся какие- то странные треугольные ячейки. Разумеется, за пять минут работы с Autodesk 3D Studio можно было бы создать идеальные шестеренки и без этого кода:
CGear::CGear(double r, double t, int teeth)
{
double twopi = 6.28318530718;
double rl = r - 0.3;
double r2 = r + 0.3;
int nFaceVert = teeth * 4;
int nVert = nFaceVert * 2;
D3DVECTOR* Vertices = new D3DVECTOR[nVert];
D3DVECTOR* pv = Vertices;
double da = twopi / (teeth * 4);
double a = 0;
for (int i = 0; i « teeth; i++) {
pv-»x = rl * cos(a);
pv-»y = rl * sin(a);
pv-»z = 0;
pv++;
a += da ;
pv-»x = r2 * cos (a) ;
pv-»y = r2 * sin(a);
pv-»z = 0;
pv++;
a += da;
pv-»x = r2 * cos(a);
pv-»y = r2 * sin(a);
pv-»z = 0;
pv++;
a += da;
pv-»x = rl * cos(a);
pv-»y = rl * sin(a);
pv-»z = 0;
pv++ ;
a += da;
}
pv = Vertices;
D3DVECTOR* pv2 = SVertices[nFaceVert] ;
for (i = 0; i « nFaceVert; i++) {
*pv2 = *pv;
pv2-»z = t;
pv++;
pv2++;
}
// Сгенерировать данные граней для зубцов.
// Нервных просят не смотреть!
int nf = (teeth * 5 * 4) + (teeth * 26) + 10;
/b>
Глава 6. Перемещение объектов в макете
int* FaceData = new int[nf] ;
int* pfd = FaceData;
for (i = 0; i « teeth*4; i++) {
*pfd++ = 4;
*pfd++ = i;
*pfd++ = (i + 1) % (teeth*4);
*pfd++ = nFaceVert + ((i + 1) % (teeth*4));
*pfd++ = nFaceVert + (i % (teeth*4));
}
// Завершить список *pfd++ = 0;
Create(Vertices, nVert, NULL, 0, FaceData, TRUE);
// Добавить торцевые грани с заданием нормалей D3DVECTOR nvect [] = {
(О, 0, 1},
(О, 0, -1} };
delete [] FaceData;
FaceData = new int [teeth * 9 + teeth * 4 + 10] ;
pfd = FaceData;
for (1=0; i « teeth; i++) {
*pfd++ = 4;
*pfd++ = i*4;
*pfd++ = 1;
*pfd++ = i*4+3;
*pfd++ = 1;
*pfd++ = i*4+2;
*pfd++ = 1;
*pfd++ = i*4+l;
*pfd++ = 1;
}
*pfd++ = teeth*2;
for (i = teeth-1; i »= 0; i-) {
*pfd++ = i*4+3;
*pfd++ = 1;
*pfd++ = i*4;
*pfd++ = 1;
}
*pfd++ = 0;
AddFaces(Vertices, nVert, nvect, 2, FaceData);
pfd = FaceData;
for (i = 0; i « teeth; i++) {
*pfd++ = 4;
Относительное движение тЩ!) 153
*pfd++ = nFaceVert + i*4;
*pfd++ = 0;
*pfd++ = nFaceVert + i*4+l;
*pfd++ = 0;
*pfd++ = nFaceVert + i*4+2;
*pfd++ = 0;
*pfd++ = nFaceVert + i*4+3;
*pfd++ = 0;
}
*pfd++ = teeth*2;
for (i = 0; i « teeth; i++) {
*pfd++ = nFaceVert + i*4;
*pfd++ = 0;
*pfd++ = nFaceVert + i*4+3;
*pfd++ = 0;
}
*pfd = 0;
AddFaces(Vertices, nVert, nvect, 2, FaceData);
delete [] Vertices;
delete [] FaceData;
SetColor(l, 1, 0);
}
К пастоящему моменту весь этот фрагмент должен казаться вам вполне понятным. Когда у меня будет немного свободного времени, я непременно сделаю на основе данного приложения настоящие часы с секундной, минутной и часовой стрелками, с циферблатом и маятником.
Перемещение объектов по произвольным траекториям
Конечно, объект можно перемещать по любой траектории. Для этого необходимо задать либо набор координат в пространстве, либо функцию (например, генератор сплайнов), которая строит плавную кривую для траектории, описанной несколькими точками. При каждой новой итерации вы определяете новое положение объекта и перемещаете его туда. Не забывайте, что вам также придется вычислять вектор направления объекта и, возможно, его верхний вектор (если только перемещаемый объект не является сферой).
Создание собственного контроллера движения
Перемещение объектов и полеты — это, конечно, хорошо, но что делать, если вам понадобится что-то другое? Давайте посмотрим, как создать контроллер движения для более интересного объекта — космического танка Mark VII с доплеров-ским радаром Х-диапаэона. Танк может передвигаться по поверхности планеты с различной скоростью и поворачивать на ходу. Его башня быстро вращается, а пушка
/b> в!^' Глава 6. Перемещение объектов в макете
поднимается. Кажется, я забыл упомянуть о радаре, который радостно вертится на башне? На Рисунок 6-11 изображен танк Mark VII при выполнении боевого задания.
Рисунок. 6'П. Космический танк Mark VII с доплеровским радаром Х-диапазона
Хмм... вы обратили внимание на то, что у танка нет колес? Могу предложить два объяснения:
• Это летающий танк.
• Мне было лень возиться с колесами.
Решайте сами.
На Рисунок 6- 12 изображена диаграмма подвижных частей танка (вместе с колесами). Иллюстрация приведена на цветной вкладке.
. Составные части танка
Создание собственного контроллера движения
/h2>
В приложении Tank класс C3dTank является производным от C3dFrame. Последовательность, в которой строится танк, такова: сначала мы присоединяем корпус к внешнему фрейму, затем присоединяем башню к корпусу и в последнюю очередь присоединяем пушку и радар к башне. Радар приводится в постоянное вращение. Пушка может подниматься и опускаться, вращаясь вокруг своей горизонтальной оси. Башня может вращаться вокруг вертикальной оси корпуса.
Перед тем как заниматься контроллером, давайте рассмотрим фрагмент кода, в котором создается танк, чтобы нам было легче управлять им:
C3dTank::C3dTank() {
// Создать фрейм
C3dFrame::Create(NULL) ;
// Загрузить составные части танка и построить танк
m_hull.Load(IDX_HULL) ;
AddChild(&m_hull) ;
m_turret.Load(IDX_TURRET) ;
m hull.AddChild(&m_turret);
m_gun.Load(IDX_GUN) ;
m_turret.AddChild(&m_gun) ;
// Радар имеет собственный фрейм,
// чтобы было удобнее управлять осью вращения
C3dFrame rframe;
rframe.Create(&m_turret);
C3dShape radar;
radar.Load(IDX_RADAR) ;
rframe.AddChild(Sradar) ;
radar.SetPosition(0, 0, -0.3);
rframe.SetPosition(О, О, 0.3);
rframe.SetRotation(0, 1, 0, 0.1);
SetGun(25) ;
}
Единственное, что может здесь показаться странным, — это то, что я использовал для радара отдельный фрейм. Мне пришлось поступить так из-за того, что в первоначальном варианте танка ось у радара была смещена относительно того места, где я хотел расположить радар. Поэтому я задал начало координат фрейма в той точке башни, где помещается ось, и сместил объект-радар внутри фрейма, чтобы он находился над осью вращения (Рисунок 6-13).
Все объекты, из которых состоит наш танк, были созданы в 3D Studio и преобразованы в формат .X с помощью утилиты conv3ds, входящей в DirectX 2 SDK. Они были включены в файл приложения RC2 в качестве ресурсов:
//
// STAGE.RC2 - resources Microsoft Visual C++ does not edit
directly
//
/b> fy Глава 6. Перемещение объектов в макете
#ifdef APSTUDIO_INVOKED
terror this file is not editable by Microsoft Visual C++
#endif //APSTUDIO_INVOKED
/////////'11/1111/111/1111111111/1111111/1' I Ullll/lt'I'III'I•II/
I I / I I / / / / / I II t / / / I
II Add manually edited resources here...
^include "3dPlus.rc"
// Tank parts
I DX_HULL XO F re S \ T_hul 1. X
IDX_TURRET XOF res\turret.x
IDX_GON XOF res\gun.x
IDX_RADAR XOF res\radar.x
camo.bmp BITMAP res\camo.bmp
camousa.bmp BITMAP res\camousa.bmp // Звуковые эффекты
IDS_BANG WAVE res\bang.wav
/////////////////////////////////////////////////////////// //////////////////
Рисунок. 6-13. Размещение радара внутри фрейма
Башня
Тэг XOF, встречающийся в файле ресурсов, на самом деле можно заменить любой другой строкой. Я выбрал XOF лишь потому, что такое расширение используется в файлах описания фигур. Единственное место программы, где встречается строка XOF — функция C3dShape::Load, где эта строка используется для того, чтобы отличать XOF-файлы от других типов ресурсов.
Создание собственного контроллера движения
/b>
В класс танка вошли три функции, находящиеся в файле 3dTank.cpp и предназначенные для регулирования углов башни и пушки, а также для стрельбы:
#define D2R 0.01745329251994
void C3dTank::SetTurret(double angle)
{
if ((angle « 0) II (angle »= 360)) { angle = 0;
} double x = sin(angle * D2R);
double z = cos(angle * D2R);
m_turret.SetDirection(x, 0, z, &m_hull);
void C3dTank::SetGun(double angle) {
if (angle « 0) ( angle = 0;
} else if (angle »= 60) ( angle = 60;
> double у = -sin(angle * D2R);
double z = cos(angle * D2R);
m gun.SetDirection(0, у, z, &m turret);
}
void C3dTank::FireGun() {
PlaySound(MAKEINTRESOURCE(IDS_BANG), AfxGetResourceHandie(), SND_RESOURCE) ;
}
Как видите, чтобы определить положение башни и пушки, мы вычисляем вектор направления. Затем мы задаем направление объекта по отношению к его родителю', кстати говоря, именно так функция SetDirection действует по умолчанию. Но я хотел сделать свой код максимально простым и очевидным, поэтому при каждом вызове передаю дополнительный аргумент — эталонный фрейм.
Танк готов. Осталось научиться управлять им.
Контроллер танка
Большая часть кода контроллера находится в классах C3dWnd и C3dController. Чтобы создать собственный контроллер, необходимо лишь ввести новый класс, производный от CSdController, переопределить в нем функцию OnUpdate и установить новый контроллер в своем приложении. Однако перед тем, как писать функцию OnUpdate, следует распределить параметры джойстика
/b> Д1' Глава 6. Перемещение объектов в макете
по выполняемым функциям. Конфигурация, на которой я остановился, приведена в табл. 6-3.
Таблица 6-3. Управление танком
Входной параметр Параметр танка
у Скорость
х Поворот
r Поворот POV (кнопка выбора вида) Направление башни
Кнопки 3 и 4 Подъем и опускание пушки
Кнопка 1 Выстрел из пушки
Я решил использовать параметры х и г для поворотов, чтобы даже при наличии самого простого джойстика с двумя осями можно было управлять танком. Я выбрал для этого приложения джойстик SideWinder Pro — он дает более реалистичные ощущения, чем SpaceBall. К тому же кнопка выбора вида, находящаяся на рукояти джойстика, замечательно подходит для поворотов башни.
Определившись с управлением, можно писать программу. Весь код контроллера состоит из двух функций:
CTankCtrl::CTankCtrl () (
m_dGunAngle = 25;
m_bWasFire = FALSE;
}
void CTankCtrl::OnUpdate(_3DIMPUTSTATE& st, C3dFrame* pFrame) {
// Задать скорость (руководствуясь значением у)
double v = st.dY / 2;
// Определить текущее положение C3dVector pos;
pFrame-»GetPosition (pos) ;
// Получить текущий вектор направления C3dVector dir, up;
pFrairie-»GetDirection(dir, up) ;
// Определить новое направление (с учетом
// параметров х и г)
double dr = -st.dX + -st.dR;
C3dMatrix r;
r.Rotate(0, dr * 3, 0) ;
dir = r * dir;
Создание собственного контроллера движения 'т^ 159
// Умножить вектор направления на скорость, // чтобы определить смещение танка C3dVector ds = dir * v;
// Задать новое положение и направление pos += ds;
pFrame-»SetPosition (pos) ;
pFrame-»SetDirection (dir) ;
// Воспользоваться информацией POV для задания
// ориентации башни.
// Для этого необходимо работать с объектом C3dTank,
// а не CSdFrame.
C3dTank* pTank = (C3dTank*) pFrame;
ASSERT (pTank-»IsKindOf (RUNTIME_CLASS (C3dTank) ) ) ;
if (st.dPov »= 0) { pTank-»SetTurret (st.dPov) ;
}
// Кнопки З и 4 поднимают и опускают пушку if (st.dwButtons & 0х04) {
m_dGunAngle += 0.1;
» if (st.dwButtons & 0х08) (
m dGunAngle -= 0.1;
} if (m_dGunAngle « 0) {
m dGunAngle =0;
} else if (m_dGunAngle » 45) {
m dGunAngle = 45;
} pTank-»SetGun (m dGunAngle);
// Проверить, не пора ли стрелять if (st.dwButtons & 0х01) { if (!m_bWasFire) 1 pTank-»FireGun () ;
m_bWasFire = TRUE;
} } else (
m_bWasFire = FALSE;
} }
Конструктор лишь инициализирует некоторые локальные данные; вся настоящая работа выполняется в функции Onllpdate. Параметр у задает текущую скорость. Текущая позиция и направление танка хранятся в объектах C3dVector. Параметры х и г определяют матрицу поворота, которая задает новую ориента-
/b> '^р? Глава 6. Перемещение объектов в макете
цию вектора направления. Вектор направления умножается на скорость — полученный вектор смещения складывается с вектором прежнего положения танка. Затем мы перемещаем танк в новое положение и задаем для танка новое направление.
Кнопка выбора вида определяет направление башни. Мы проверяем состояние кнопок 3 и 4, и если они нажаты, то угол подъема пушки изменяется на небольшую величину. Если держать одну из этих кнопок нажатой, башня будет медленно подниматься или опускаться.
Остается лишь учесть кнопку стрельбы. Проверка локальной переменной m_bWasFire предотвращает повторные выстрелы при нажатой кнопке — автоматическое оружие в США запрещено.
Окончательная сборка приложения
За основу приложения Tank был взят код приложения Moving. Я удалил ненужные команды меню и заменил текущую фигуру объектом CSdTank. Кроме того, я включил в макет фоновое изображение. Ниже приведен фрагмент кода, в котором происходит настройка главного окна приложения:
int CMainFrame::OnCreate(LPCREATESTRUCT IpCreateStruct) {
// Загрузить фоновое изображение m_imgBkgnd.Load(IDB BKGND) ;
NewScene() ;
ASSERT(m_pScene) ;
// Создать объект-контроллер m_pController = new CTankCtrl;
m_pController-»Create (&m wnd3d,
OnGetCtrlFrame,
this) ;
// Восстановить конфигурацию контроллера m_pController-»SelectDevice (m_iObjCtrlDev) ;
return 0;
}
Функция NewScene создает макет и задает начальные условия
BOOL CMainFrame::NewScene() (
// Удалить макет, если он уже существует if (m_p3cene) {
m_wnd3d.SetScene(NULL) ;
delete m_pScene;
m_pScene = NULL;
?
Создание собственного контооллеоа движения
/b>
// Создать исходный макет m_pScene = new CSdScene;
if ( !m_pScene-»Create() ) return FALSE;
// Задать источники света C3dDirLight dl;
dl.Create (0.8, 0.8, 0.8);
m pScene-»AddChild(&dl) ;
dl.SetPosition(-2, 2, -5);
dl.SetDirection(l, -1, 1);
m_pScene-»SetAmbientLight(0.4, 0.4, 0.4);
// Установить положение и направление камеры // в исходное состояние m_pScene-»SetCameraPosition (C3dVector (0, 5, -25));
m_pScene-»SetCameraDirection (C3dVector (О, О, 1));
m_wnd3d.SetScene(m_pScene) ;
// Задать фоновое изображение m pScene-»SetBackground(&m_imgBkgnd) ;
// Разместить танк в макете if (!m_pTank) m_pTank = new C3dTank;
m_pScene-»AddChild(m_pTank) ;
m_pTank-»SetPosition(0, 0, 0) ;
m_pTank-»SetDirection(0, 0, 1) ;
return TRUE;
}
Если танк уедет за край окна и потеряется, можно выполнить команду Fite ¦New, чтобы вызвать функцию NewScene и начать все заново. Осталось сказать о последнем изменении, внесенном мной, — когда контроллер запрашивает указатель на фрейм, с которым он должен работать, функция OnGetCtrlFrame возвращает ему указатель на танк:
C3dFrame* CMainFrame::OnGetCtrlFrame(void* pArg) (
CMainFrame* pThis = (CMainFrame*) pArg;
ASSERT(pThis) ;
ASSERT <pThis-»IsKindOf(RUNTIME_CLASS (CMainFrame) ) ) ;
return pThis-»m_pTank;
}
Обратите внимание — хотя функция должна возвращать указатель на C3dFrame, на самом деле она передает указатель на объект C3dTank. Мы пользуемся этим обстоятельством в функции Onllpdate, приведенной на стр. 144. Если раньше вам могло показаться, что преобразование указателя на C3dFrame в указатель на
/b> Ш^ Глава 6. Перемещение объектов в макете
CSdTank выглядит сомнительно, то теперь нетрудно убедиться, что мы имели полное право поступать таким образом.
Пора в дорогу
Хватит возиться с самолетами, часами и танками. Пойдем дальше и посмотрим, как выбираются объекты в макете.