Введение в физическую библиотеку Tokamak
Автор: maths_fan
Во многих современных играх используются сторонние физические движки. Хорошо, если ваша компания может себе позволить иметь собственную физику, либо купить физический движок. Но если вы только начинаете, а хочется, чтобы в игре была сносная физика, то воспользуйтесь Tokamak — бесплатный физический движок, который можно использовать в коммерческих целях, только лишь указав, что вы используете именно его.
Совсем недавно вышла новая версия Tokamak (надеюсь, что дальше новые версии будут появляться чаще), в которой был исправлен существенный недостаток (раньше движущиеся объекты иногда могли неправильно взаимодействовать со статическими объектами, например, в гонках колесо могло запросто попасть под дорогу, да и вообще машина могла упасть под трассу, хотя дальше физика считалась вполне правильно, и ничего нельзя было сделать с этим). Сейчас всё работает корректно, хотя о недостатках новой версии мы узнаем позже. В этом примере я хочу показать, как создавать объект движка, добавлять в него статические и движущиеся тела, приводить всё это в действие. Для простоты я взяла кубы, которые падают под действием силы гравитации на горизонтальную поверхность. Но на их месте могли бы быть другие тела (например, загруженные модели), под действием других сил, о которых можно подробнее прочитать в документации по Tokamak.
Подключение нужных библиотек.
Скачать последнюю версию бесплатного физического движка Tokamak можно с официальной страницы: Удалено
Нужно добавить папки в MSVC.
Visual Studio 6:
· Tools -> Options в меню MSVC
· Выбрать вкладку Directories
· Убедиться в том, что выбрано “Include files” в “Show directories for”
· Дважды щёлкните по пустому месту в этом окне, его можно будет редактировать
· Нажмите на “…” кнопку, чтобы выбрать место, куда вы установили SDK. Здесь нужно выбрать папку Include SDK.
· Нажмите OK
Visual Studio .Net:
· Tools -> Options
· Войти во вкладку Projects
· Перейти на VC++ Directories
· Убедиться в том, что выбрано “Include files” в “Show directories for”
· Дважды щёлкните по пустому месту в этом окне, его можно будет редактировать
· Нажмите на “…” кнопку, чтобы выбрать место, куда вы установили SDK. Здесь нужно выбрать папку Include SDK.
· Нажмите OK
То же самое нужно проделать для библиотек (выбрав в “Show directories for” “Libraries”).
Введение.
Движок различает два типа объектов – физикализируемые (rigid) и анимируемые обычными методами (animated). Для первых движком просчитываются все законы физики (которые будут указаны), вторые же рисуются и движутся только благодаря пользователю (но влияют на динамические объекты).
В движке много различных параметров, которые влияют на то, как он будет работать. Есть главный класс движка (neSimulator), он используется для доступа ко всем частям движка и обновлению текущей позиции всех объектов.
Инициализация.
Включаем заголовочный файл и подключаем библиотеку:
#include <tokamak.h> #pragma comment(lib, "tokamak.lib")
Объявим некоторые глобальные переменные и константы. Первые три – кол-во кубиков, размеры куба и пола. Последние – указатели на классы самого движка, статических и движущихся тел.
#define CUBES_NUM 5 #define CUBES_SIZE 1.0f #define FLOOR_SIZE 10.0f neSimulator* pSim = 0; neRigidBody* pCubes[CUBES_NUM]; neAnimatedBody* pFloor = 0;
Сделаем функцию инициализации движка. В ней создадим сам движок, создадим объекты движка, зададим их положения, массы, установим силы (в данном примере, одну силу – силу гравитации).
void InitPhysics() { // описывает геометрию любого тела: куб, шар, цилиндр или их объединения neGeometry *geom; // размер куба (длина, ширина и высота) neV3 boxSize1; // вектор силы гравитации neV3 gravity; // позиция – координаты центра тела neV3 pos; // масса f32 mass; // структура, нужная для инициализации движка neSimulatorSizeInfo sizeInfo; // заполняем структуру sizeInfo.rigidBodiesCount = CUBES_NUM; sizeInfo.animatedBodiesCount = 1; s32 totalBody = sizeInfo.rigidBodiesCount + sizeInfo.animatedBodiesCount; sizeInfo.geometriesCount = totalBody; // максимальное допустимое количество столкновений sizeInfo.overlappedPairsCount = totalBody * ( totalBody - 1) / 2; sizeInfo.rigidParticleCount = 0; sizeInfo.constraintsCount = 0; sizeInfo.terrainNodesStartCount = 0; // установка значения вектора гравитации – сила притяжения //направлена вниз и имеет величину // 10 Ньютон gravity.Set( 0.0f, -10.0f, 0.0f); // создаём движок – вызов статической функции pSim = neSimulator::CreateSimulator( sizeInfo, NULL, &gravity); for ( int i = 0; i < CUBES_NUM; i++) { // создаём движущееся тело pCubes[i] = pSim->CreateRigidBody( ); // нужно задать геометрию тела, для начала добавим такое свойство как геометрия geom = pCubes[i]->AddGeometry( ); // устанавливаем размеры куба boxSize1.Set( CUBES_SIZE, CUBES_SIZE, CUBES_SIZE); // нужно установить только что созданную геометрию geom->SetBoxSize( boxSize1[0], boxSize1[1], boxSize1[2]); // изменения вступают в действие pCubes[i]->UpdateBoundingInfo( ); mass = 1.0f; // инертность тела - заставляет после соударения закручиваться // для кубов есть класс neBoxInertiaTensor, // который сам вычисляет инертность по размеру и массе куба pCubes[i]->SetInertiaTensor( neBoxInertiaTensor( boxSize1[0], boxSize1[1], boxSize1[2], mass)); // устанавливаем массу pCubes[i]->SetMass( mass); // задаём позицию pos.Set( ( float)( rand( )%10) / 100, 4.0f + i * CUBES_SIZE, ( float)( rand( )%10) / 100); // устанавливаем позицию pCubes[i]->SetPos( pos); } // аналогичным образом создаём статический объект pFloor = pSim->CreateAnimatedBody( ); geom = pFloor->AddGeometry( ); boxSize1.Set( FLOOR_SIZE, 0.2f, FLOOR_SIZE); geom->SetBoxSize( boxSize1[0],boxSize1[1],boxSize1[2]); pFloor->UpdateBoundingInfo( ); pos.Set( 0.0f, -3.0f, 0.0f); pFloor->SetPos( pos); }
Небольшие пояснения по инициализации.
// максимальное допустимое количество столкновений sizeInfo.overlappedPairsCount = totalBody * (totalBody - 1) / 2;
В нашем примере все тела могут столкнуться одновременно, поэтому такое значение. Если бы тела, например, находились в космическом пространстве, на большом расстоянии, то было бы естественно поставить это значение достаточно маленьким.
В качестве статического объекта мы хотим задать плоскость, но плоскости не поддерживаются движком (что и понятно, так как плоскости не имеют объёма), поэтому аналогично движущимся телам делаем параллелепипед, но с небольшой высотой.
Обновление положения тел.
Чтобы передвинуть тела, в движке нужно вызвать метод Advance, у которого единственным параметром является время в секундах с прошлого вызова функции.
Для определения этого параметра напишем функцию:
float GetElapsedTime() { static int oldTime = GetTickCount( ); int newTime = GetTickCount( ); float result = ( newTime - oldTime) / 1000.0f; oldTime = newTime; return result; }
По рекомендациям разработчиков движка нужно ограничиться тем, что интервал времени, на который мы хотим сдвинуть все объекты не должен отклоняться более, чем на 20% от предыдущего. И не должен превышать 1/45-ю секунды.
void UpdateObjects() { static float oldElapsed; float newElapsed = GetElapsedTime( ); if ( oldElapsed != 0.0f) { // проверяем отклонение на 20% выше и ниже (120% и 80%) if ( newElapsed < 0.8f * oldElapsed) newElapsed = 0.8f * oldElapsed; if ( newElapsed > 1.2f * oldElapsed) newElapsed = 1.2f * oldElapsed; // на 1/45-ю секунды if ( newElapsed > 1.0f/45.0f) newElapsed = 1.0f/45.0f; } oldElapsed = newElapsed; pSim->Advance( newElapsed); }
Отрисовка.
Задать положение можно разными способами. В OpenGL телу задаются некоторые координаты и чтобы повернуть, либо сдвинуть тело, нужно координаты умножить на соответствующие матрицы поворота и сдвига.
То есть, чтобы повернуть куб вокруг осей на a, b, c, с центром в x, y, z нужно сделать следующее:
// инструкции выполняются снизу вверх из-за способа перемножения матриц glTranslated(x, y, z); glRotated( c, 0.0, 0.0, 1.0); glRotated( b, 0.0, 1.0, 0.0); glRotated( a, 1.0, 0.0, 0.0); // рисуем куб, как будто он в центре координат и расположен // параллельно осям //...
При таком методе рисования нам не нужны сами координаты сдвинутого объекта, нужны только матрица поворота и вектор сдвига. Метод GetTransform() любого тела движка возвращает как раз матрицу поворота и вектор сдвига, упакованные в одну структуру neT3.
void DrawCubes() { // хранит значение, возвращаемое GetTransform кубика neT3 t; // матрица 4х4 для OpenGL, включает в себя вектор сдвига и / //матрицу поворота float matrix[16]; // повторяем процедуру для каждого кубика for ( int i = 0; i < CUBES_NUM; i++) { // получаем данные для кубика t = pCubes[i]->GetTransform( ); // переводим их в данные, пригодные для использования в // OpenGL // копируем матрицу поворота matrix[0] = t.rot[0][0]; matrix[4] = t.rot[0][1]; matrix[8] = t.rot[0][2]; matrix[1] = t.rot[1][0]; matrix[5] = t.rot[1][1]; matrix[9] = t.rot[1][2]; matrix[2] = t.rot[2][0]; matrix[6] = t.rot[2][1]; matrix[10] = t.rot[2][2]; // копируем вектор сдвига matrix[12] = t.pos[0]; matrix[13] = t.pos[1]; matrix[14] = t.pos[2]; // остальное matrix[3] = 0.0f; matrix[7] = 0.0f; matrix[11] = 0.0f; matrix[15] = 1.0f; // теперь рисуем // сохраняем матрицу преобразований вершин OpenGL, // чтобы не влиять на другие кубики и пол glPushMatrix( ); // умножаем текущую матрицу на полученную glMultMatrixf( matrix); // Рисуем куб в центре координат заданных размеров Cube( CUBES_SIZE, CUBES_SIZE, CUBES_SIZE); // Возвращаем матрицу в прежний вид glPopMatrix( ); } }
Теперь аналогичным образом рисуем пол.
void DrawFloor() { neT3 t; float matrix[16]; t = pFloor->GetTransform( ); matrix[0] = t.rot[0][0]; matrix[4] = t.rot[0][1]; matrix[8] = t.rot[0][2]; matrix[1] = t.rot[1][0]; matrix[5] = t.rot[1][1]; matrix[9] = t.rot[1][2]; matrix[2] = t.rot[2][0]; matrix[6] = t.rot[2][1]; matrix[10] = t.rot[2][2]; // копируем вектор сдвига matrix[12] = t.pos[0]; matrix[13] = t.pos[1]; matrix[14] = t.pos[2]; // остальное... matrix[3] = 0.0f; matrix[7] = 0.0f; matrix[11] = 0.0f; matrix[15] = 1.0f; glPushMatrix( ); glMultMatrixf( matrix); Cube( FLOOR_SIZE, 0.2f, FLOOR_SIZE); glPopMatrix( ); }
Сама функция рисования будет выглядеть так:
void DrawOpenGL() { // обновляем положение тел UpdateObjects( ); // чистим экран и сбрасываем матрицу преобразований glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity( ); // Отодвигаем сцену от камеры, чтобы все было видно glTranslated( 0.0, 0.0, -5.0); // Рисуем объекты сцены, уже обновленные DrawCubes( ); DrawFloor( ); // обновляем содержимое экрана SDL_GL_SwapBuffers( ); }
Отрисовка куба.
Уверена, что каждый может сделать это самостоятельно, но чтобы можно было не останавливаться, а сразу же опробовать пример, приведу отрисовку куба с заданными размерами и в начале координат.
void Cube(float length, float width, float height) { length /= 2; width /= 2; height /= 2; GLfloat v0[] = { -length, -width, height }; GLfloat v1[] = { length, -width, height }; GLfloat v2[] = { length, width, height }; GLfloat v3[] = { -length, width, height }; GLfloat v4[] = { -length, -width, -height }; GLfloat v5[] = { length, -width, -height }; GLfloat v6[] = { length, width, -height }; GLfloat v7[] = { -length, width, -height }; static GLubyte red[] = { 255, 0, 0, 255 }; static GLubyte green[] = { 0, 255, 0, 255 }; static GLubyte blue[] = { 0, 0, 255, 255 }; static GLubyte white[] = { 255, 255, 255, 255 }; static GLubyte yellow[] = { 0, 255, 255, 255 }; static GLubyte black[] = { 0, 0, 0, 255 }; static GLubyte orange[] = { 255, 255, 0, 255 }; static GLubyte purple[] = { 255, 0, 255, 0 }; glBegin( GL_TRIANGLES); glColor4ubv( red); glVertex3fv( v0); glColor4ubv( green); glVertex3fv( v1); glColor4ubv( blue); glVertex3fv( v2); glColor4ubv( red); glVertex3fv( v0); glColor4ubv( blue); glVertex3fv( v2); glColor4ubv( white); glVertex3fv( v3); glColor4ubv( green); glVertex3fv( v1); glColor4ubv( black); glVertex3fv( v5); glColor4ubv( orange); glVertex3fv( v6); glColor4ubv( green); glVertex3fv( v1); glColor4ubv( orange); glVertex3fv( v6); glColor4ubv( blue); glVertex3fv( v2); glColor4ubv( black); glVertex3fv( v5); glColor4ubv( yellow); glVertex3fv( v4); glColor4ubv( purple); glVertex3fv( v7); glColor4ubv( black); glVertex3fv( v5); glColor4ubv( purple); glVertex3fv( v7); glColor4ubv( orange); glVertex3fv( v6); glColor4ubv( yellow); glVertex3fv( v4); glColor4ubv( red); glVertex3fv( v0); glColor4ubv( white); glVertex3fv( v3); glColor4ubv( yellow); glVertex3fv( v4); glColor4ubv( white); glVertex3fv( v3); glColor4ubv( purple); glVertex3fv( v7); glColor4ubv( white); glVertex3fv( v3); glColor4ubv( blue); glVertex3fv( v2); glColor4ubv( orange); glVertex3fv( v6); glColor4ubv( white); glVertex3fv( v3); glColor4ubv( orange); glVertex3fv( v6); glColor4ubv( purple); glVertex3fv( v7); glColor4ubv( green); glVertex3fv( v1); glColor4ubv( red); glVertex3fv( v0); glColor4ubv( yellow); glVertex3fv( v4); glColor4ubv( green); glVertex3fv( v1); glColor4ubv( yellow); glVertex3fv( v4); glColor4ubv( black); glVertex3fv( v5); glEnd( ); }
Освобождение ресурсов.
При выходе из программы, нужно не забыть «удалить» движок.
void KillPhysics() { if ( pSim) { neSimulator::DestroySimulator( pSim); pSim = 0; } }
#физика, #Tokamak, #библиотеки, #движок, #программирование физики, #физический движок
25 июня 2005 (Обновление: 17 мая 2024)