Верле: разрешаем коллизии (часть 2 — сетка, квадратики)

Вспоминаем

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

Возникает закономерный вопрос: зачем проверять на пересечение окружности, которые находятся далеко друг от друга?

Действительно, теперь мы будем поступать чуть хитрее.

Сетка

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

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

Справа для каждой окружности написано, в каких соседних клетках лежат другие окружности, с которыми возможно пересечение

Справа для каждой окружности написано, в каких соседних клетках лежат другие окружности, с которыми возможно пересечение

Дальше нам останится проверить наличие коллизий между нашей окружностью и всеми 'кандидатами' и при наличии таковых — разрешить их как в прошлый раз.

Напишем класс для сетки, основываясь на всем этом:

Grid.hpp

// constants::gridStep = 2 * radiusOfObject

// Класс ячейки чисто ради удобства
struct Cell {
  std::vector cellObjects;

  Cell() : cellObjects{} {}
};

struct CollisionGrid {
  // Сетка будет больше экрана на определенную величину с каждой стороны
  // Чтобы объект, например, подлетевший высоко наверх
  // за экран, мог вернуться назад
  const int width =
      (constants::screenWidth + 2 * constants::worldBorderFromScreen) /
      constants::gridStep;
  const int height =
      (constants::screenWidth + 2 * constants::worldBorderFromScreen) /
      constants::gridStep;

  std::vector> grid;

  CollisionGrid() : grid(height, std::vector(width)) {}

  // Я решил втупую заново заполнять сетку после каждой
  // итерации цикла движка - самый простой способ
  void updateGrid(std::vector &objects) {
    for (int x = 0; x < width; ++x) {
      for (int y = 0; y < height; ++y) {
        grid[y][x].cellObjects.clear();
      }
    }

    auto object = std::begin(objects);
    while (object++ != std::end(objects)) {
      if (*object == nullptr || object == std::end(objects))
        return;
      
      // Не забываем прибавить worldBorderFromScreen, чтобы
      // перейти в систему отсчета экрана
      int objx =
          (*object)->positionCurrent.x + constants::worldBorderFromScreen;
      int objy =
          (*object)->positionCurrent.y + constants::worldBorderFromScreen;

      // Если объект за границами 'мира' - удаляем его
      if (objx / constants::gridStep >= width ||
          objy / constants::gridStep >= height || objx < 0 || objy < 0) {
        objects.erase(object);
        continue;
      }

      grid[objy / constants::gridStep][objx / constants::gridStep]
          .cellObjects.push_back(*object);
    }
  }

  // Для читаемости кода в основном методе
  bool isCollideable(VerletObject *object1, VerletObject *object2) {
    if (object1 == object2 || !object1->isMoveable)
      return false;
    return true;
  }

  // Разрешении коллизии осталось аналогичным
  void collide(VerletObject *object1, VerletObject *object2) {
    Vec2 collisionAxis =
        object1->positionCurrent - object2->positionCurrent;
    const float dist = collisionAxis.length();

    if (dist > object1->radius + object2->radius)
      return;

    Vec2 normalized = collisionAxis / dist;
    const float delta = object1->radius + object2->radius - dist;

    object1->positionCurrent += normalized * delta * 0.5f;
    object2->positionCurrent -= normalized * delta * 0.5f;
  }

  // Сюда мы будем передавать координаты ячейки, на которую смотрим
  void collidecell(int x, int y) {
    // Для каждого ее объекта
    for (auto object1 : grid[y][x].cellObjects) {
      // В каждой из соседних ячеек
      for (int dx = -1; dx < 2; ++dx) {
        for (int dy = -1; dy < 2; ++dy) {
          // Для каждого из объектов, лежащих в них
          for (auto object2 : grid[y + dy][x + dx].cellObjects) {
            // Проверяем коллизию
            if (!isCollideable(object1, object2))
              continue;
            collide(object1, object2);
          }
        }
      }
    }
  }
};

Осталось лишь добавить эту сетку в класс движка и написать метод для ее обхода:

Тут немного

private:
  CollisionGrid grid;
//...

//...
void solveCollisions(){
  for(int x = 1; x < grid.width - 1; ++x) {
    for(int y = 1; y < grid.height - 1; ++y) {
      grid.collidecell(x, y);
    }
  }
}

//...

//...
void update(float dt) {
  //...
  solveCollisions();
  grid.updateGrid(objects);
  //...
}

Отлично! Самое время перейти от примивных тел к чему-то более сложному.

Link.hpp

Теперь напишим линки — жесткие связи между объектами. Так мы получим возможность создавать различные структуры.

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

c68746235c51d4fd65f1f0150e36d6a4.pngТак и запишем

struct Link {
  // Будем хранить ссылки на объекты, которые линк связывает
  eng::VerletObject *object_1;
  eng::VerletObject *object_2;
  
  // Длина линка
  float targetDist;

  // Для его отображения
  sf::Vertex line[2];
  
  // Конструктор
  Link(eng::VerletObject *obj_1, eng::VerletObject *obj_2, float targDist)
      : object_1{obj_1}, object_2{obj_2}, targetDist{targDist} {}

  // Применяем рассуждение с картинки
  void apply() {
    const eng::Vec2 axis =
        object_1->positionCurrent - object_2->positionCurrent;
    const float dist = axis.length();
    const eng::Vec2 normalized = axis / dist;
    const float delta = targetDist - dist;
    object_1->positionCurrent += normalized * delta * 0.5f;
    object_2->positionCurrent -= normalized * delta * 0.5f;
    
    line[0] = sf::Vertex(
        sf::Vector2f(object_1->positionCurrent.x, object_1->positionCurrent.y),
        sf::Color::White);
    line[1] = sf::Vertex(
        sf::Vector2f(object_2->positionCurrent.x, object_2->positionCurrent.y),
        sf::Color::White);
  }
};

Закономерно добавляем в класс движка публичный метод для добавления линка и приватный метод для вызова apply () от всех линков, который будем вызывать каждый кадр (в update ()).

Тут все просто

//...
  void applyLinks() {
    if (!links.size())
      return;

    for (auto *link : links) {
      link->apply();
    }
  }

void drawLinks() {
    if (!links.size())
      return;
    for(auto *link: links) {
      window->draw(link->line, 2, sf::Lines);
    }
  }
//...
//...
void update(float dt) {
//...
    applyLinks();
//...
}
//...
void addLink(eng::VerletObject *obj_1, eng::VerletObject *obj_2,
               float targDist) {
    Link *link = new Link(obj_1, obj_2, targDist);
    links.push_back(link);
  }
//...

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

Можно сделать и за один проход, но так нагляднее

  void addSquare(float pos_x, float pos_y, float size, sf::Color color) {

    int objectsInRow = size / constants::objRadius / 2 + 1;
    float linklength = constants::objRadius * 2.f;
    std::vector> square(
        objectsInRow, std::vector(objectsInRow));

    for (float y = pos_y, i = 0; y <= pos_y + size;
         y += constants::objRadius * 2, ++i) {
      for (float x = pos_x, j = 0; x <= pos_x + size;
           x += constants::objRadius * 2, ++j) {

        addObject(x, y, constants::objRadius, false, color);
        square[i][j] = objects[objects.size() - 1];
      }
    }


    for (int y = 0; y < objectsInRow; ++y) {
      for (int x = 0; x < objectsInRow; ++x) {
        if (x < objectsInRow - 1)
          addLink(square[y][x], square[y][x + 1], linklength);
        if (y >= 1)
          addLink(square[y][x], square[y - 1][x], linklength);
        if (y >= 1 && x >= 1)
          addLink(square[y][x], square[y - 1][x - 1], linklength * sqrt(2));
      }
    }
  }

Посмотрим, что получается:

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

Update’им метод update

//...
void update(float dt) {
    int iterations = constants::physicSteps; // у меня - 8
    dt = dt / static_cast(iterations);
    while (iterations--) {
      applyGravity();
      applyLinks();
      applyConstraint();
      solveCollisions();
      updatePositions(dt);
    }
    drawObjects();
    drawLinks();
  }
//...

Что теперь? Теперь наша симуляция может выглядить (по сравнению с тем, что было в первой части) достаточно интересно.

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

Спойлер: симуляция на 8 потоках

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

Всем спасибо, кто дочитал!

© Habrahabr.ru