Основні класи cannon-es — підтримуваного форку Cannon.js — для побудови фізики твердих тіл у браузері: World, Body, Shape, Material, Constraint, фільтрація колізій, події контактів, і точний патерн синхронізації тіл з мешами Three.js щокадру.
CANNON.World — контейнер верхнього рівня: гравітація,
broadphase, солвер і списки тіл/constraints/contact materials
живуть у ньому. Викликайте world.step(dt) раз на кадр
анімації, щоб просунути симуляцію.
| Властивість / метод | Тип | Призначення |
|---|---|---|
gravity |
Vec3 | Постійне прискорення, що діє на всі динамічні тіла |
broadphase |
NaiveBroadphase | SAPBroadphase | Стратегія відсіювання пар перед narrow phase |
solver.iterations |
number | Кількість ітерацій PGS-солвера за крок (за замовч. 10) |
defaultContactMaterial |
ContactMaterial | Тертя/пружність, коли для пари не задано окремий материал |
world.addBody(body) |
метод | Зареєструвати Body у світі |
world.addConstraint(c) |
метод | Зареєструвати Constraint (шарнір) |
world.step(dt, time, maxSubSteps) |
метод | Просунути симуляцію на dt секунд (фіксований крок + інтерполяція) |
import * as CANNON from 'cannon-es';
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.82, 0),
});
world.broadphase = new CANNON.SAPBroadphase(world);
world.solver.iterations = 10;
// Виклик step() з фіксованим кроком у циклі рендеру
const fixedTimeStep = 1 / 60;
function animate() {
requestAnimationFrame(animate);
world.step(fixedTimeStep);
renderer.render(scene, camera);
}
CANNON.Body представляє одне тверде тіло: масу,
позицію, кватерніон (орієнтацію), швидкість, кутову швидкість і
одну чи кілька приєднаних форм. Тіло з mass: 0 —
статичне — нескінченна маса, ніколи не рухається,
але бере участь у колізіях.
| Властивість | Тип | Примітки |
|---|---|---|
mass |
number | 0 = статичне, >0 = динамічне |
position |
Vec3 | Центр мас у світових координатах |
quaternion |
Quaternion | Орієнтація — копіюйте напряму у mesh.quaternion |
velocity / angularVelocity |
Vec3 | Лінійна / кутова швидкість у світових координатах |
type |
Body.DYNAMIC | STATIC | KINEMATIC | KINEMATIC тіла рухаються скриптом, не реагують на сили |
linearDamping / angularDamping |
number 0–1 | Загасання швидкості за крок (наближення опору повітря) |
fixedRotation |
boolean | Фіксує орієнтацію — корисно для персонажів/предметів, які не мають перекидатися |
allowSleep / sleepSpeedLimit |
boolean / number | Тіла у спокої «засинають» — солвер пропускає їх, доки їх не потривожать |
ccdSpeedThreshold |
number | Вмикає безперервне виявлення колізій вище цієї швидкості (запобігає тунелюванню) |
const body = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 5, 0),
shape: new CANNON.Sphere(0.5),
linearDamping: 0.01,
});
body.applyForce(new CANNON.Vec3(0, 0, 50), body.position);
body.applyImpulse(new CANNON.Vec3(5, 0, 0), body.position);
world.addBody(body);
Одне Body може нести кілька форм (складені тіла),
кожна з опціональним локальним offset і
orientation через
body.addShape(shape, offset, quaternion).
| Форма | Конструктор | Застосування |
|---|---|---|
| Box | new Box(new Vec3(hx,hy,hz)) | Половини розмірів — ящики, стіни, платформи |
| Sphere | new Sphere(radius) | Найдешевша перевірка narrow phase — м'ячі, частинки |
| Cylinder | new Cylinder(rTop, rBottom, h, segments) | Бочки, колеса (у парі з HingeConstraint) |
| ConvexPolyhedron | new ConvexPolyhedron({vertices, faces}) | Довільні опуклі меші — імпорт з Three.js BufferGeometry |
| Trimesh | new Trimesh(vertices, indices) | Тільки статична неопукла геометрія — рельєф, меші рівня |
| Heightfield | new Heightfield(matrix, {elementSize}) | Рельєф на основі сітки — дешевше за Trimesh для ландшафтів |
| Plane | new Plane() | Нескінченна статична площина — земля, невидимі стіни |
| Particle | new Particle() | Точкова маса нульового розміру — вузли тканини/мотузки |
Trimesh і Heightfield можна приєднати
лише до тіл з mass: 0 — SAT-базований narrow phase
cannon-es вимагає опуклості для динамічних тіл. Для динамічного
неопуклого об'єкта розкладіть його на кілька опуклих форм на
одному складеному тілі (наприклад, за допомогою інструмента
декомпозиції на опуклі оболонки).
Material — це просто іменована мітка на формі/тілі;
фактичні числа тертя та пружності живуть у
ContactMaterial, який поєднує два материали разом.
const ice = new CANNON.Material('ice');
const rubber = new CANNON.Material('rubber');
const iceRubberContact = new CANNON.ContactMaterial(ice, rubber, {
friction: 0.02, // низьке — вільно ковзає
restitution: 0.3, // 0 = без відскоку, 1 = ідеально пружний
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3,
});
world.addContactMaterial(iceRubberContact);
const puck = new CANNON.Body({ mass: 1, material: rubber, shape: new CANNON.Cylinder(0.4,0.4,0.1,16) });
const rink = new CANNON.Body({ mass: 0, material: ice, shape: new CANNON.Plane() });
Constraints (шарніри) обмежують відносний рух двох тіл. Усі
constraints додаються до світу через
world.addConstraint(c).
| Constraint | Поведінка |
|---|---|
| PointToPointConstraint | Прикріплює локальну точку A до локальної точки B — кулькові шарніри, мотузки |
| HingeConstraint | Одна обертальна ступінь свободи навколо спільної осі — двері, колеса, плечі маятника |
| DistanceConstraint | Фіксована відстань між двома тілами — жорсткі сегменти мотузки |
| LockConstraint | Повністю зварює два тіла разом (без відносного руху) |
| ConeTwistConstraint | Кульковий шарнір з обмеженням кута конуса — плечі/стегна ragdoll |
// Маятник: шарнір, що з'єднує ящик з фіксованою точкою підвісу
const anchor = new CANNON.Body({ mass: 0 });
anchor.position.set(0, 5, 0);
world.addBody(anchor);
const hinge = new CANNON.HingeConstraint(anchor, bobBody, {
pivotA: new CANNON.Vec3(0, 0, 0),
pivotB: new CANNON.Vec3(0, 1.5, 0),
axisA: new CANNON.Vec3(0, 0, 1),
axisB: new CANNON.Vec3(0, 0, 1),
});
world.addConstraint(hinge);
Кожна форма несе collisionFilterGroup (до якої групи
вона належить) і collisionFilterMask (з якими
групами вона має перевірятись) — обидва бітові маски. Дві форми
зіштовхуються тільки якщо (A.group & B.mask) і
(B.group & A.mask) обидва ненульові.
const GROUP_PLAYER = 1; // 0b0001
const GROUP_ENEMY = 2; // 0b0010
const GROUP_TERRAIN = 4; // 0b0100
const GROUP_TRIGGER = 8; // 0b1000 — без фізичної реакції, лише події
playerBody.collisionFilterGroup = GROUP_PLAYER;
playerBody.collisionFilterMask = GROUP_ENEMY | GROUP_TERRAIN | GROUP_TRIGGER;
// Вороги зіштовхуються з рельєфом, але проходять крізь одне одного
enemyBody.collisionFilterGroup = GROUP_ENEMY;
enemyBody.collisionFilterMask = GROUP_PLAYER | GROUP_TERRAIN;
| Подія | Спрацьовує на | Дані |
|---|---|---|
'collide' |
body | { body, target, contact } |
'sleep' / 'wakeup' |
body | Тіло увійшло/вийшло зі стану сну |
'postStep' |
world | Спрацьовує після кожного world.step() — гарне місце для зчитування оновлених позицій |
playerBody.addEventListener('collide', (e) => {
const impactSpeed = e.contact.getImpactVelocityAlongNormal();
if (impactSpeed > 5) playLandingSound(impactSpeed);
});
Cannon-es не має власної концепції рендерингу — щокадру копіюйте
position і quaternion кожного тіла
напряму у відповідний Three.js mesh. Обидві
бібліотеки використовують однакову конвенцію осей і структуру
кватерніона, тож жодних перетворень не потрібно.
const bodies = []; // { body: CANNON.Body, mesh: THREE.Mesh }
function addBox(size, mass, position) {
const shape = new CANNON.Box(new CANNON.Vec3(size.x/2, size.y/2, size.z/2));
const body = new CANNON.Body({ mass, shape, position: new CANNON.Vec3(...position) });
world.addBody(body);
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(size.x, size.y, size.z),
new THREE.MeshStandardMaterial({ color: 0x60a5fa }),
);
scene.add(mesh);
bodies.push({ body, mesh });
return body;
}
function animate() {
requestAnimationFrame(animate);
world.step(1/60);
// Синхронізувати кожен меш з фізичним станом його тіла
for (const { body, mesh } of bodies) {
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
}
renderer.render(scene, camera);
}
world.step(dt, deltaTime, maxSubSteps) приймає
фактичний минулий час другим аргументом і виконає додаткові
фіксовані під-кроки внутрішньо, щоб «наздогнати» — викликайте
його як world.step(1/60, realDeltaSeconds, 5), щоб
фізика лишалась детермінованою навіть при різній частоті
оновлення екрана (60Гц проти 144Гц проти зависання вкладки).
mass: 0.
applyForce, накопичуються і скидаються автоматично
щокроку world.step() — але ручні цикли інтегрування,
що пропускають step(), накопичуватимуть силу
нескінченно.
world.solver.iterations, увімкніть
ccdSpeedThreshold/ccdSweptSphereRadius,
або розбийте симуляцію на під-кроки (див.
туторіал про фізичний рушій).
world.allowSleep, і
перевірте, чи contactEquationStiffness не занадто
високий для вашого кроку часу — надто жорсткі контакти можуть
«перестрибувати» і осцилювати.
scale — закладайте масштаб у розміри форми при
створенні (наприклад, множте половини розмірів Box), а не через
трансформацію мешу.