Skip to content

149. cannon 物理引擎:铰链和向心力

Published:

前面用过 cannon 物理引擎的推力、摩擦力、弹力等,但物理世界可不止这些作用力。

还有旋转的向心力等。

比如旋转秋千这种就是向心力:

image.png

这节我们就来做一个向心力的案例:摆锤。

npx create-vite cannon-hingle-pendulum

image.png

进入项目,安装依赖:

pnpm install
pnpm install --save three
pnpm install --save-dev @types/three

安装 cannon:

pnpm install --save cannon-es

改下 src/main.js

import './style.css';
import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);

scene.add(mesh);

scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const dir = new THREE.DirectionalLight(0xffffff, 0.8);
dir.position.set(5, 10, 5);

scene.add(dir);

const width = window.innerWidth;
const height = window.innerHeight;

const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
camera.position.set(0, 8, 8);
camera.lookAt(0, 2, 0);

const renderer = new THREE.WebGLRenderer({
  antialias: true,
});
renderer.setSize(width, height)

function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

render();

document.body.append(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

创建 Scene、Light、Camera、Renderer。

改下 style.css

body {
  margin: 0;
}

写一下 mesh.js

import * as THREE from 'three';
import * as THREE from 'three';

const group = new THREE.Group();

const groundGeo = new THREE.PlaneGeometry(20, 20);
const groundMat = new THREE.MeshLambertMaterial({ color: 0x2c3e50 });
const groundMesh = new THREE.Mesh(groundGeo, groundMat);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.receiveShadow = true;
group.add(groundMesh);

export default group;

我们先画一个平面。

看下效果:

npm run dev

image.png

2025-11-23 12.37.37.gif

然后把摆锤画出来:

image.png


const pivotY = 5;
const pivotGeo = new THREE.SphereGeometry(0.15, 16, 16);
const pivotMat = new THREE.MeshPhongMaterial({ color: 'blue' });
const pivotMesh = new THREE.Mesh(pivotGeo, pivotMat);
pivotMesh.position.set(0, pivotY, 0);
group.add(pivotMesh);

const pendulumRadius = 0.3;
const ropeLength = 3;
const pendulumY = pivotY - ropeLength;

const pendulumGeo = new THREE.SphereGeometry(pendulumRadius, 32, 32);
const pendulumMat = new THREE.MeshPhongMaterial({ color: 'orange' });
const pendulumMesh = new THREE.Mesh(pendulumGeo, pendulumMat);
pendulumMesh.position.set(0, pendulumY, 0);
pendulumMesh.castShadow = true;
group.add(pendulumMesh);

const ropeMat = new THREE.LineBasicMaterial({ color: 'white' });
const ropeGeo = new THREE.BufferGeometry().setFromPoints([
  new THREE.Vector3(0, pivotY, 0),
  new THREE.Vector3(0, pendulumY, 0)
]);
const ropeLine = new THREE.Line(ropeGeo, ropeMat);
group.add(ropeLine);

两个球、一条线连接。

看下效果:

2025-11-23 12.40.52.gif

然后如何让黄球旋转、篮球固定呢?

这就需要用到一个新的铰链 api:

首先我们创建 cannon 物理世界:

image.png

import * as CANNON from 'cannon-es';

const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);

下面下面的平面加到物理世界里:

image.png

const groundBody = new CANNON.Body({ mass: 0 });
groundBody.addShape(new CANNON.Plane());
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(groundBody);

质量为 0 就是不会移动。

然后分别把固定点、小球也加到物理世界里:

image.png

const pivotBody = new CANNON.Body({ mass: 0, position: new CANNON.Vec3(0, pivotY, 0) });
pivotBody.addShape(new CANNON.Sphere(0.15));
world.addBody(pivotBody);

质量为 0,和地面一样是固定不动的。

image.png

const pendulumBody = new CANNON.Body({
    mass: 2,
    position: new CANNON.Vec3(0, pendulumY, 0),
});
pendulumBody.addShape(new CANNON.Sphere(pendulumRadius));
world.addBody(pendulumBody);

如何让固定点、小球连接起来呢?

用铰链 api:

image.png

const hingeAxis = new CANNON.Vec3(0, 0, 1);
const hinge = new CANNON.HingeConstraint(pivotBody, pendulumBody, {
  pivotA: new CANNON.Vec3(0, 0, 0),
  pivotB: new CANNON.Vec3(0, ropeLength, 0),
  axisA: hingeAxis,
  axisB: hingeAxis
});
world.addConstraint(hinge);

添加铰链约束,把固定点和球连接起来。

pivotA、pivotB 分别指定铰链连接两个物体的什么位置,这里设置成他们的 position 即可,也就是连接中心点。

axisA 和 axiesB 是旋转轴方向,0,0,1 就是绕 z 轴旋转。

然后我们把物理世界的物体位置同步到 threejs 场景:

image.png

function animate() {
    requestAnimationFrame(animate);
    world.fixedStep();
  
    pendulumMesh.position.copy(pendulumBody.position);
    pendulumMesh.quaternion.copy(pendulumBody.quaternion);
  
    const positions = ropeLine.geometry.attributes.position;
    positions.setXYZ(0, pivotMesh.position.x, pivotMesh.position.y, pivotMesh.position.z);
    positions.setXYZ(1, pendulumMesh.position.x, pendulumMesh.position.y, pendulumMesh.position.z);
    positions.needsUpdate = true;  
}

animate();

主要是同步小球的位置、旋转角度,线的端点位置这两个。

线的两个端点分别是小球和固定点的位置,设置 needsUpdate 为 true 提示 threejs 更新了顶点位置。

最后,给小球一个推力看下效果:

image.png

const impulse = new CANNON.Vec3(15, 0, 0);
pendulumBody.applyImpulse(impulse, pendulumBody.position);

2025-11-23 13.13.09.gif

可以看到,小球绕固定点摆动起来了。

加大下推力试试:

image.png

这样,摆锤的效果就实现了。

案例代码上传了小册仓库

总结

这节我们学了铰链约束的 api,他可以把两个物体通过铰链连接起来,实现一起运动的效果。

后面遇到推门、摆锤这种物理效果,都可以用铰链来实现。

评论