Это третья статья из моих руководств по 3D-физике Javascript. Прежде чем продолжить, предполагается, что вы прочитали первые две статьи. Если нет, я настоятельно рекомендую вам это сделать. Первая статья - Введение в 3D-физику JavaScript с использованием Ammo.js и three.js, а вторая - Перемещение объектов в 3D-физике JavaScript с использованием Ammo.js и three.js

Вступление

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

К счастью, ammo.js предоставляет концепции, помогающие обнаруживать столкновения. У нас есть

  • Проверка контактного коллектора
  • ContactTest
  • ContactPairTest
  • Призрачный объект

Для простоты мы будем рассматривать только проверку контактного коллектора, ContactTest и ContactPairTest.

Прежде чем мы начнем, не забудьте настроить свое рабочее пространство. Это четко указано в первом руководстве и цитируется ниже:

Сначала получите библиотеки для three.js и ammo.js. Three.js можно получить с https://threejs.org/build/three.js, а для ammo.js загрузите репо с https://github.com/kripken/ammo.js, затем перейдите в папку сборки для файла ammo.js.

Создайте папку своего проекта и дайте ей любое имя по вашему выбору. В нем создайте файл index.html и папку «js», которая должна содержать файлы three.js и ammo.js.

Обратите внимание, что это руководство, как и предыдущие, черпает много информации из руководства пользователя bullet и ныне несуществующей вики по физике bullet. В частности, это руководство содержит некоторые фрагменты кода, взятые из примера выпуклого разрыва по физике three.js.

Внимание: вам придется много копировать и вставлять, если вы будете следовать этому руководству. И не пугайтесь, эта статья не такая объемная, как кажется.

Проверка контактного коллектора

Чтобы помочь нам лучше понять проверку контактного коллектора, мы объясним некоторые концепции.

Контактное лицо

Как следует из названия, это точка соприкосновения двух объектов столкновения. Точка контакта представлена ​​как btManifoldPoint в ammo.js и предоставляет полезную информацию о контакте, такую ​​как импульс и положение. Он также обеспечивает расстояние между двумя объектами. Предполагается, что для контакта расстояние между объектами всегда будет равно нулю, однако оно может быть больше или меньше.

Контактный коллектор

«Контактный коллектор - это кэш, содержащий все точки контакта между парами объектов столкновения». Для двух сталкивающихся объектов контактный коллектор содержит все точки соприкосновения между ними. Он представлен в ammo.js как btPersistentManifold и предоставляет некоторые полезные методы, например, для получения двух сталкивающихся объектов, количества точек контакта между ними, а также конкретной точки контакта по индексу.

Широкая фаза

Broadphase определенно не новость для нас, мы впервые столкнулись с ним в первом уроке, и он был частью структур, которые мы используем при инициализации нашего физического мира:

overlappingPairCache = new Ammo.btDbvtBroadphase()

Однако мы разберемся с этим немного больше. Broadphase обеспечивает быстрый и оптимизированный способ устранения пар объектов столкновения на основе перекрытия их выровненной по оси ограничительной рамки (AABB). По сути, для каждой пары объектов столкновения в мире широкофазный алгоритм проверяет, перекрываются ли их AABB. Пара сохраняется, если есть перекрытие, или отклоняется в противном случае, тем самым создавая приблизительный список сталкивающихся пар. Однако есть пары, которые имеют перекрывающиеся AABB, но все еще недостаточно близки для столкновения. Они обрабатываются позже более конкретными алгоритмами действий.

Две основные структуры широкофазного ускорения доступны в ammo.js btDbvtBroadphase (динамическое дерево AABB) и btAxisSweep3 (Sweep and Prune или SAP).

btDbvtBroadphase «динамически адаптируется к размерам мира и его содержимому. Это очень хорошо оптимизированный и очень хороший широкофазный универсальный инструмент. Он обрабатывает динамические миры, в которых многие объекты находятся в движении, а добавление и удаление объектов происходит быстрее, чем SAP ».

«Dbvt» в названии означает дерево динамического ограничивающего объема.

btAxisSweep3 с другой стороны, «также является хорошей широкоформатной версией общего назначения с ограничением, заключающимся в том, что он требует фиксированного размера мира, известного заранее. Эта широкая фаза лучше всего подходит для типичных динамических миров, где большинство объектов практически не движется ».

Диспетчер

Мы также все время использовали объект диспетчера:

dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration)

После того, как широкая фаза завершена, удаляя неперекрывающиеся AABB, диспетчер «выполняет итерацию по каждой паре, ищет соответствующий алгоритм коллизий на основе типов задействованных объектов и выполняет алгоритм коллизий, вычисляя точки контакта».

Чтобы собрать все это вместе, вот упрощенное описание того, как все они подходят.
На каждом шаге моделирования широкофазный (btDbvtBroadphase или btAxisSweep3) проверяет каждую пару объектов столкновения в мире, отфильтровывая те, которые не перекрывают AABB. Затем диспетчер (btCollisionDispatcher) применяет подробный алгоритм коллизий к каждой паре, предоставляя нам коллекторы контактов (btPersistentManifold), которые содержат одну или несколько точек контакта (btManifoldPoint).

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

Пример объяснил бы лучше.

Создайте новый файл в своей рабочей области и назовите его contact_manifold_check.html. Скопируйте в него приведенный ниже код для начальной загрузки

Просмотрите файл в своем браузере, и у вас должно получиться что-то похожее на изображение ниже.

То, что у вас есть, - это стена из сетки. Мышь можно перемещать, и нажатие левой кнопки мыши выбрасывает мяч из позиции курсора. За один раз может быть выброшен только один мяч со сроком службы три (3) секунды, и гравитация отсутствует. Точно так же мы добавили теги к объектам сетки three.js через их свойство userData: wall.userData.tag = "wall"; и ball.userData.tag = "ball";. Это поможет нам идентифицировать их в процессе.

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

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

Чтобы определить, произошло ли столкновение, мы будем после каждого моделирования:

  • Получите диспетчера.
  • У диспетчера узнать количество сгенерированных контактных коллекторов.
  • Итерируйте, чтобы получить каждый контактный коллектор.
  • Для каждого контактного коллектора получите количество содержащихся точек контакта.
  • Итерируйте, чтобы получить каждую точку контакта.
  • Для каждой точки контакта получите расстояние.
  • Наконец, запишите эту информацию в консоль.

Вернемся к коду для внесения некоторых изменений.

После определения updatePhysics() вставьте приведенное ниже

Вышеупомянутый метод detectCollision() представляет собой практически построчную реализацию шагов, перечисленных ранее. Добавьте вызов этого метода в последнюю строку updatePhysics(), при этом updatePhysics() теперь будет выглядеть так:

Идите вперед и просмотрите работу в браузере. Откройте консоль браузера, чтобы увидеть, что печатается при ударе мяча о стену.

Четкое наблюдение из записанных выходных данных показывает, что некоторые из отображаемых расстояний больше нуля. Да, это потому, что два объекта могут быть очень близки, чтобы их можно было принять, есть контакт, но между ними все еще очень маленькое расстояние. Мы отфильтруем все такие случаи, то есть каждую точку контакта, расстояние которой больше нуля.

Внутри detectCollision(), чуть выше строки, ведущей к консоли, добавьте

if( distance > 0.0 ) continue;

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

Ваш код должен выглядеть вот так.

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

В этой и двух предыдущих статьях мы использовали свойство userData объектов three.js. Это позволяет пользователю добавлять дополнительные свойства к объекту three.js, который будет извлекаться или использоваться позже. Мы использовали это для хранения ссылки на объекты ammo.js в их соответствующих объектах three.js;

ball.userData.physicsBody = ballPhysicsBody;

С этим мы можем получить физический объект позже, как это сделано в updatePhysics().

С другой стороны, Ammo.js, производный от своего родительского проекта bullet, также имеет способ добавить ссылку пользовательского объекта на физический объект. Это означает, что мы также можем сделать так, чтобы наши физические объекты ссылались на соответствующий объект three.js. Это достигается с помощью методов setUserPointer() и getUserPointer() btCollisionObject (родительский класс btRigidBody). Благодаря этому мы можем получить наши объекты three.js, если у нас есть физические объекты под рукой.

Так и должно быть. Однако мы будем использовать природу javascript, добавляя наш объект three.js напрямую как свойство физических объектов без setUserPointer() и извлекая их так же просто. Если вы ничего из этого не понимаете, просто продолжите обучение, вы ничего не пропустите.

Вернуться к работе.

Помните, что теперь мы должны идентифицировать участвующие объекты и извлекать информацию из их точек контакта. Что мы будем делать

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

Переходим к коду. Перейдите к createWall() и добавьте следующую строку кода в качестве последней.

body.threeObject = wall;

Также в createBall() добавьте приведенный ниже код перед оператором возврата.

body.threeObject = ball;

Это устанавливает ссылку на наши объекты three.js как свойства соответствующих им физических объектов.

Теперь перейдите к detectCollision() сразу после строки, в которой

let contactManifold = dispatcher.getManifoldByIndexInternal( i );

Добавить

let rb0 = Ammo.castObject( contactManifold.getBody0(), Ammo.btRigidBody );
let rb1 = Ammo.castObject( contactManifold.getBody1(), Ammo.btRigidBody );
let threeObject0 = rb0.threeObject;
let threeObject1 = rb1.threeObject;
if ( ! threeObject0 && ! threeObject1 ) continue;
let userData0 = threeObject0 ? threeObject0.userData : null;
let userData1 = threeObject1 ? threeObject1.userData : null;
let tag0 = userData0 ? userData0.tag : "none";
let tag1 = userData1 ? userData1.tag : "none";

В приведенном выше фрагменте кода физические объекты извлекаются с использованием getBody0() и getBody1(), после чего получается их соответствующий объект three.js. Для этих объектов three.js мы получаем значение их тега. Я хотел бы отметить, что и getBody0(), и getBody1() возвращают btCollisionObject, поэтому мы привели их к btRigidBody.

Все еще в detectCollision(), после строки кода, в которой

if( distance > 0.0 ) continue;

Добавить

let velocity0 = rb0.getLinearVelocity();
let velocity1 = rb1.getLinearVelocity();
let worldPos0 = contactPoint.get_m_positionWorldOnA();
let worldPos1 = contactPoint.get_m_positionWorldOnB();
let localPos0 = contactPoint.get_m_localPointA();
let localPos1 = contactPoint.get_m_localPointB();

Наконец, замените строку журнала консоли, которая

console.log({manifoldIndex: i, contactIndex: j, distance: distance});

с участием

console.log({
 manifoldIndex: i, 
 contactIndex: j, 
 distance: distance, 
 object0:{
  tag: tag0,
  velocity: {x: velocity0.x(), y: velocity0.y(), z: velocity0.z()},
  worldPos: {x: worldPos0.x(), y: worldPos0.y(), z: worldPos0.z()},
  localPos: {x: localPos0.x(), y: localPos0.y(), z: localPos0.z()}
 },
 object1:{
  tag: tag1,
  velocity: {x: velocity1.x(), y: velocity1.y(), z: velocity1.z()},
  worldPos: {x: worldPos1.x(), y: worldPos1.y(), z: worldPos1.z()},
  localPos: {x: localPos1.x(), y: localPos1.y(), z: localPos1.z()}
 }
});

Ваша работа должна выглядеть так: это

Сделайте паузу, чтобы немного поразмышлять. Код довольно хорошо объясняет себя.

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

Это все для проверки контактного коллектора. Теперь вы можете добавить дополнительные реализации в соответствии с вашими потребностями.

Контакты

Ammo.js позволяет вам «выполнять мгновенный запрос мира (btCollisionWorld или btDiscreteDynamicsWorld) с помощью запроса contactTest. Запрос contactTest выполнит тест на столкновение для всех перекрывающихся объектов в мире и выдаст результаты с помощью обратного вызова. Объект запроса не обязательно должен быть частью мира ».

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

«Преимущество этого метода в том, что вы можете выполнять тесты столкновений с уменьшенным временным разрешением, если вам не нужны тесты столкновений при каждом физическом тике». Например, вы можете захотеть проверять столкновение только при щелчке мышью или при нажатии клавиши. Возможно, в вашей игре вы не хотите, чтобы персонаж прыгал в воздухе, поэтому всякий раз, когда нажимается клавиша прыжка, вызывается contactTest, чтобы проверить, контактирует ли персонаж с каким-либо физическим объектом.

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

ConcreteContactResultCallback

Как упоминалось ранее, contactTest принимает объект обратного вызова как часть своих параметров для обработки результатов контакта. Этот объект обратного вызова в ammo.js - ConcreteContactResultCallback. Чтобы использовать его, вам нужно будет добавить реализацию его метода addSingleResult() method, который вызывается при контакте.

Приведем пример тренировки.

Создайте новый файл с именем contact_test.html, затем скопируйте и вставьте приведенный ниже код:

Предварительный просмотр файла в браузере покажет что-то вроде

У нас есть пол, состоящий из четырех цветных плиток и шара, который можно перемещать с помощью клавиш WASD. Будьте осторожны, чтобы не упасть с края.

Просматривая код, вы заметите, что у физических объектов уже есть ссылка на их аналоги в three.js, добавленные как свойство threeObject. Вы также заметите, что мы добавили обработчик нажатия клавиши для T-key, который вызывает checkContact(), который на данный момент ничего не делает.

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

Сначала давайте создадим наш ConcreteContactResultCallback объект и добавим реализацию для его addSingleResult() метода. Перейдите в раздел объявления переменных вверху кода и добавьте

let cbContactResult;

Это будет дескриптор нашего объекта ConcreteContactResultCallback.

Следующий. Перед определением moveBall() добавить

Пауза. Давайте сделаем несколько пояснений. Обратите внимание на нашу реализацию addSingleResult() выше. Требуемая подпись метода:

float addSingleResult( 
      [Ref] btManifoldPoint cp, 
      [Const] btCollisionObjectWrapper colObj0Wrap, 
      long partId0, 
      long index0, 
      [Const] btCollisionObjectWrapper colObj1Wrap, 
      long partId1, 
      long index1 )

Для каждого установленного контакта мир физики вызывает addSingleResult() со значениями параметров (возвращаемое значение не имеет значения). cp - это дескриптор точки контакта, а colObj0Wrap и colObj1Wrap - дескрипторы для оберток объектов столкновения, из которых мы можем извлечь участвующие объекты. Из-за того, что ammo.js переносится из bullet с помощью emscripten, эти значения, которые должны быть объектами ammo.js, фактически передаются как то, что мы можем справедливо назвать указателями. Чтобы получить реальные значения, нам придется использовать метод wrapPointer(), который поставляется с ammo.js (подробнее о нем вы можете прочитать здесь).

Остальные параметры partId0, index0, partId1 и index1 нам не нужны. Честно говоря, самое близкое, что я знаю об их использовании, - это что-то вроде «объединителя материалов для каждого треугольника / нестандартного материала», и я уверен, что нам это не понадобится.

Наша реализация addSingleResult() проста и понятна:

  • Определите расстояние от точки контакта и выйдите, если расстояние больше нуля.
  • Получите участвующие физические объекты.
  • От них получают соответствующие объекты three.js.
  • Помня, что мы находимся сразу за плитками, мы проверяем объект three.js, который не является мячом, и присваиваем переменные соответствующим образом.
  • Наконец, с некоторым форматированием мы записываем информацию в консоль.

Еще не время предварительного просмотра. Перейти к методу start(), после вызова createBall() добавить:

setupContactResultCallback();

Перейдите на checkContact() и добавьте

physicsWorld.contactTest( ball.userData.physicsBody , cbContactResult );

Это вызывает contactTest() физического мира для обнаружения столкновения с физическим объектом шара. Любой результат будет передан addSingleResult() из cbContactResult.

С учетом всего сказанного у вас должен быть код, похожий на this.

Просмотрите в браузере, переместите мяч, нажмите клавишу T и наблюдайте за информацией, которая отображается на консоли.

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

Помните, что ваш объект запроса (в нашем случае мяч) не обязательно должен быть частью физического мира, чтобы contactTest работал. Однако вам придется обновить преобразование самостоятельно, чтобы получить точные результаты.

Контакты

Это похоже на contactTest, за исключением того, что вам нужно будет предоставить два физических объекта для проверки контакта между ними. Параметры contactPairTest - это два объекта столкновения и объект обратного вызова для обработки результатов контакта.

Как и в случае с contactTest, два физических объекта не обязательно должны быть частью физического мира.

Чтобы продемонстрировать тест контактной пары, мы продолжим использовать наш код из contactTest.

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

Вернитесь к нашему коду. Прямо в разделе объявления переменных под строкой, в которой let cbContactResult; добавлено

let redTile, cbContactPairResult;

redTile будет дескриптором красной плитки, а cbContactPairResult, как и cbContactResult, будет нашим объектом обратного вызова для contactPairTest.

Чтобы установить ручку красной плитки, мы бы сделали несколько не очень чистых, но простительных доработок. Перейдите к методу createFloorTiles(), в последней строке цикла for..of добавьте

if( tile.name == "red"){
  mesh.userData.physicsBody = body;
  redTile = mesh;
}

Затем давайте создадим обратный вызов. После setupContactResultCallback() определения скопируйте и вставьте

Все, что нам нужно, это чтобы addSingleResult() устанавливал значение hasContact при каждом контакте. Обратите внимание, что hasContact не существует в ConcreteContactResultCallback, мы просто добавили его сами как свойство.

Перед переходом к следующей модификации добавьте вызов setupContactPairResultCallback() внутри start() сразу после setupContactResultCallback().

Теперь о функции прыжка. Вставьте приведенный ниже код перед определением updatePhysics()

В приведенном выше методе мы сначала сбрасываем свойство hasContact объекта cbContactPairResult на false, а затем вызываем физический мир contactPairTest(). Остальная часть кода говорит сама за себя.

Наконец, нам нужно изменить обработчик нажатия клавиши для вызова jump() при нажатии J-клавиши. Добавьте приведенную ниже запись в качестве новой записи дела в структуру switch..case handleKeyDown(), желательно сделать ее последней.

case 74://J
  jump();
  break;

handleKeyDown() теперь должен выглядеть как

Ваш окончательный код должен выглядеть как этот.

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

Вот и все; реализация contactPairTest.

Заключение

Вот это да!! Какая поездка.

Не стесняйтесь изменять коды, исследовать и экспериментировать по своему усмотрению. Не забудьте обратиться к ammo.idl, включенному в репозиторий ammo.js, чтобы узнать больше о классах и интерфейсах, предоставляемых ammo.js.

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

Ваше здоровье.