Физика жидкостей: частичная эмуляция через силы

Библиотека Oimo.js — это физический движок для трёхмерных твёрдых тел. Он реализует столкновения, импульсную динамику, ограничения, трение и упругость. Однако полноценная гидродинамика (Navier–Stokes, давление, вязкость, поверхностное натяжение) в движке отсутствует.

Следовательно, моделирование жидкостей возможно только через частичную эмуляцию с использованием:

  • сил плавучести,
  • сил сопротивления (drag),
  • направленных импульсов,
  • демпфирования,
  • большого числа мелких тел.

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


Архитектура физического мира в Oimo.js

В основе лежит объект мира:

const world = new OIMO.World({
    gravity: [0, -9.8, 0],
    timestep: 1/60,
    iterations: 8,
    broadphase: 2
});

Гравитация задаётся глобально, но жидкостная эмуляция требует локального переопределения сил для отдельных тел.

Тело создаётся так:

const body = world.add({
    type: 'box',
    size: [1, 1, 1],
    pos: [0, 5, 0],
    move: true,
    density: 1
});

Все жидкостные эффекты реализуются через методы:

  • body.applyForce(forceVec, positionVec)
  • body.applyImpulse(positionVec, impulseVec)
  • изменение linearDamping
  • изменение angularDamping

Принцип плавучести

Теоретическая основа

Сила Архимеда:

[ F = {жидкости} V{погружённой части} g]

В Oimo.js нет автоматического вычисления объёма погружённой части, поэтому рассчитывается:

  • глубина погружения,
  • относительный объём,
  • результирующая подъёмная сила.

Реализация зоны жидкости

Создаётся логическая область (например, бассейн):

const waterLevel = 0;
const waterDensity = 2;

Каждый кадр выполняется проверка положения тела:

function applyBuoyancy(body) {

    const position = body.getPosition();
    const halfHeight = 0.5;

    const depth = waterLevel - (position.y - halfHeight);

    if (depth > 0) {

        const submergedRatio = Math.min(depth / (halfHeight * 2), 1);

        const volume = 1; // для куба 1x1x1
        const buoyancyForce = waterDensity * volume * 9.8 * submergedRatio;

        body.applyForce([0, buoyancyForce, 0], position);
    }
}

Важные моменты

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

Сопротивление жидкости (Drag)

Жидкость создаёт сопротивление, пропорциональное скорости:

[ F_{drag} = -k v]

Реализация:

function applyDrag(body) {

    const velocity = body.getLinearVelocity();

    const dragCoefficient = 2;

    const dragForce = [
        -velocity.x * dragCoefficient,
        -velocity.y * dragCoefficient,
        -velocity.z * dragCoefficient
    ];

    body.applyForce(dragForce, body.getPosition());
}

Альтернативный способ — использование встроенного демпфирования:

body.linearDamping = 0.9;
body.angularDamping = 0.9;

Разница:

  • демпфирование — упрощённая модель,
  • ручной расчёт drag даёт более контролируемое поведение.

Частичная эмуляция объёма через набор частиц

Другой подход — представить жидкость как множество мелких сфер.

for (let i = 0; i < 200; i++) {
    world.add({
        type: 'sphere',
        size: [0.2],
        pos: [
            Math.random()*2 - 1,
            5 + Math.random()*2,
            Math.random()*2 - 1
        ],
        move: true,
        density: 0.2,
        friction: 0,
        restitution: 0
    });
}

Особенности:

  • большое количество тел нагружает broadphase,
  • необходимо уменьшать iterations,
  • отключать трение,
  • уменьшать restitution.

Такой метод создаёт эффект «вязкой массы», но не даёт реалистичных волн.


Эмуляция вязкости

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

Простейшая модель:

function applyViscosity(body, neighbors) {

    const pos = body.getPosition();
    const k = 0.5;

    neighbors.forEach(n => {

        const nPos = n.getPosition();

        const dx = nPos.x - pos.x;
        const dy = nPos.y - pos.y;
        const dz = nPos.z - pos.z;

        const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);

        if (distance < 1) {

            body.applyForce([
                dx * k,
                dy * k,
                dz * k
            ], pos);
        }
    });
}

Это создаёт эффект притяжения и сглаживания движения.


Волновой эффект через импульсы

Для имитации волн используется передача импульса от одного тела к соседнему:

body.applyImpulse(
    body.getPosition(),
    [0, 5, 0]
);

Если импульс передаётся группе частиц, возникает волнообразное движение.

Для бассейна с фиксированными стенками:

  • создаются статические тела (move: false),
  • частицы сталкиваются с ними,
  • возникает отражение движения.

Поверхностное натяжение

Простейшая аппроксимация:

  • притяжение частиц на границе,
  • увеличение силы при малом числе соседей.

Логика:

  1. Подсчитать соседей в радиусе.
  2. Если их меньше порога — применить силу к центру массы группы.

Это создаёт тенденцию жидкости сохранять компактную форму.


Производительность и оптимизация

При большом числе частиц необходимо:

  • уменьшать timestep,
  • ограничивать максимальную скорость тел,
  • отключать лишние столкновения,
  • использовать sleep: true для покоящихся объектов,
  • уменьшать broadphase сложность.

Пример ограничения скорости:

const maxSpeed = 10;

const v = body.getLinearVelocity();
const speed = Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z);

if (speed > maxSpeed) {
    body.setLinearVelocity([
        v.x / speed * maxSpeed,
        v.y / speed * maxSpeed,
        v.z / speed * maxSpeed
    ]);
}

Комбинированная модель жидкости

Практически применимая схема:

  1. Зона воды с расчётом плавучести.
  2. Drag + повышенное демпфирование.
  3. Ограничение скорости.
  4. Дополнительные импульсы для волн.
  5. Частицы только для поверхностного слоя.

Такой гибрид:

  • значительно дешевле полноценного SPH,
  • визуально правдоподобен,
  • хорошо работает в браузере.

Ограничения метода

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

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