Программирование игр, создание игрового движка, OpenGL, DirectX, физика, форум
GameDev.ru / Программирование / Статьи / Введение в физическую библиотеку Tokamak

Введение в физическую библиотеку Tokamak

Автор:

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

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

Изображение

Подключение нужных библиотек.

Скачать последнюю версию бесплатного физического движка Tokamak можно с официальной страницы: http://www.tokamakphysics.com/

Нужно добавить папки в 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;
  }
}

25 июня 2005

#физика, #Tokamak, #библиотеки, #движок, #программирование физики, #физический движок


Обновление: 3 сентября 2009

2001—2018 © GameDev.ru — Разработка игр