Физические симуляции

Two.js — это мощная библиотека для создания двумерной графики на основе HTML5 Canvas, SVG и WebGL. Несмотря на то, что она не предоставляет встроенный полноценный физический движок, она позволяет реализовывать физические симуляции через интеграцию с внешними библиотеками или собственную логику на JavaScript. Физические симуляции в Two.js включают управление объектами с учетом сил, массы, гравитации, столкновений и трения.


Представление объектов для симуляции

В Two.js каждый визуальный элемент создается как объект Two.Shape или его производные (Two.Circle, Two.Rectangle, Two.Path). Для физической симуляции каждому объекту обычно добавляют следующие свойства:

  • mass — масса объекта, влияет на инерцию.
  • velocity — вектор скорости {x, y}.
  • acceleration — вектор ускорения {x, y}.
  • forces — список приложенных сил, которые накапливаются каждый кадр.

Пример расширения объекта круга физическими свойствами:

const circle = two.makeCircle(100, 100, 50);
circle.mass = 2;
circle.velocity = { x: 0, y: 0 };
circle.acceleration = { x: 0, y: 0 };
circle.forces = [];

Применение сил

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

[ = m = ]

Пример добавления силы гравитации:

function applyGravity(shape, gravity = 0.98) {
    shape.forces.push({ x: 0, y: gravity * shape.mass });
}

function computeAcceleration(shape) {
    let ax = 0;
    let ay = 0;
    shape.forces.forEach(force => {
        ax += force.x / shape.mass;
        ay += force.y / shape.mass;
    });
    shape.acceleration.x = ax;
    shape.acceleration.y = ay;
    shape.forces = [];
}

Обновление движения

После вычисления ускорения, положение и скорость объектов обновляются через численное интегрирование. Наиболее простой метод — явный метод Эйлера:

function updateMotion(shape, deltaTime) {
    shape.velocity.x += shape.acceleration.x * deltaTime;
    shape.velocity.y += shape.acceleration.y * deltaTime;
    shape.translation.x += shape.velocity.x * deltaTime;
    shape.translation.y += shape.velocity.y * deltaTime;
}

deltaTime — время между кадрами в секундах. Для более точных симуляций можно использовать методы Рунге-Кутты.


Столкновения и отражения

Для столкновений объектов нужно определять пересечение их границ. Для примера с кругами проверка расстояния между центрами:

function checkCollision(circle1, circle2) {
    const dx = circle2.translation.x - circle1.translation.x;
    const dy = circle2.translation.y - circle1.translation.y;
    const distance = Math.sqrt(dx * dx + dy * dy);
    return distance < (circle1.radius + circle2.radius);
}

При обнаружении столкновения можно использовать импульсную реакцию:

[ ’ = - (1 + e) ]

где e — коэффициент упругости, n — нормаль между объектами, v и u — скорости объектов до столкновения.

Простейшее отражение вдоль нормали:

function resolveCollision(circle1, circle2, restitution = 0.9) {
    const dx = circle2.translation.x - circle1.translation.x;
    const dy = circle2.translation.y - circle1.translation.y;
    const distance = Math.sqrt(dx * dx + dy * dy);
    const nx = dx / distance;
    const ny = dy / distance;

    const relativeVelocityX = circle1.velocity.x - circle2.velocity.x;
    const relativeVelocityY = circle1.velocity.y - circle2.velocity.y;
    const dot = relativeVelocityX * nx + relativeVelocityY * ny;

    const impulse = (-(1 + restitution) * dot) / (1 / circle1.mass + 1 / circle2.mass);
    circle1.velocity.x += (impulse * nx) / circle1.mass;
    circle1.velocity.y += (impulse * ny) / circle1.mass;
    circle2.velocity.x -= (impulse * nx) / circle2.mass;
    circle2.velocity.y -= (impulse * ny) / circle2.mass;
}

Трение и сопротивление

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

function applyDamping(shape, damping = 0.98) {
    shape.velocity.x *= damping;
    shape.velocity.y *= damping;
}

Для трения о поверхность применяется сила, противоположная направлению движения:

function applyFriction(shape, mu = 0.1) {
    const speed = Math.sqrt(shape.velocity.x**2 + shape.velocity.y**2);
    if(speed > 0){
        shape.forces.push({
            x: -mu * shape.mass * (shape.velocity.x / speed),
            y: -mu * shape.mass * (shape.velocity.y / speed)
        });
    }
}

Организация симуляции

Движение всех объектов обновляется внутри анимационного цикла Two.js:

two.bind('update', function(frameCount, deltaTime) {
    deltaTime = deltaTime / 1000; // преобразование в секунды
    shapes.forEach(shape => {
        applyGravity(shape);
        applyFriction(shape);
        computeAcceleration(shape);
        updateMotion(shape, deltaTime);
    });
    
    // Проверка столкновений
    for(let i = 0; i < shapes.length; i++){
        for(let j = i+1; j < shapes.length; j++){
            if(checkCollision(shapes[i], shapes[j])){
                resolveCollision(shapes[i], shapes[j]);
            }
        }
    }
}).play();

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


Интеграция с внешними физическими движками

Для более сложных симуляций Two.js легко сочетается с библиотеками, такими как:

  • Matter.js — мощный 2D-движок с точной обработкой столкновений и мягких тел.
  • Planck.js — реализация Box2D на JavaScript для физики с телами и пружинами.

Объекты Two.js при этом служат только визуальной оболочкой, а физика рассчитывается движком. Синхронизация происходит через обновление shape.translation в каждом кадре по координатам физического тела.


Использование constraints и soft-body эффектов

В более продвинутых симуляциях можно моделировать соединенные объекты:

  • DistanceConstraint — фиксированное расстояние между двумя объектами.
  • Spring — пружинное соединение с коэффициентами жесткости и демпфирования.

В Two.js их реализация возможна через прямое управление координатами объектов на основе рассчитанных сил и импульсов.


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

  • Минимизировать количество объектов на сцене при использовании численных интеграторов.
  • Использовать requestAnimationFrame (что автоматически делает Two.js) для оптимизации рендеринга.
  • В сложных симуляциях применять spatial partitioning (например, квадродерево) для ускорения проверки столкновений.
  • Избегать тяжелых операций внутри цикла update, предварительно рассчитывая постоянные величины.

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