Skip to content

150. cannon 物理引擎:旋转、向心力

Published:

上节学了铰链,但并没有体现出向心力来,这节我们来做一个能体现向心力的案例:

一个旋转的转盘,上面一堆立方体,随着旋转,立方体会被甩出去。

接下来实现下:

创建项目:

npx create-vite cannon-hingle-platform

image.png

进入项目,安装依赖:

pnpm install
pnpm install --save three
pnpm install --save-dev @types/three
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.add(mesh);

scene.background = new THREE.Color(0x0f2027);

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

const helper = new THREE.AxesHelper(1000);
scene.add(helper);

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

const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
camera.position.set(8, 5, 8);
camera.lookAt(0, 1, 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';

const group = new THREE.Group();

const pillarHeight = 2;
const platformRadius = 3;
const platformHeight = 0.2;
const platformY = pillarHeight;

const platformGeo = new THREE.CylinderGeometry(platformRadius, platformRadius, platformHeight);
const platformMat = new THREE.MeshPhongMaterial({ color: 0x00bcd4 });
const platformMesh = new THREE.Mesh(platformGeo, platformMat);
platformMesh.position.set(0, platformY, 0);
group.add(platformMesh);

export default group;

画一个圆形的台面

看下效果:

npm run dev

image.png

2025-11-23 13.50.02.gif

然后下面加上柱子:

image.png

const pillarHeight = 2;
const pillarRadius = 0.3;
const pillarGeo = new THREE.CylinderGeometry(pillarRadius, pillarRadius, pillarHeight, 16);
const pillarMat = new THREE.MeshPhongMaterial({ color: 0x607d8b, metalness: 0.5 });
const pillarMesh = new THREE.Mesh(pillarGeo, pillarMat);
pillarMesh.position.set(0, pillarHeight / 2, 0);
group.add(pillarMesh);

2025-11-23 13.52.10.gif

还有下面的地面:

image.png

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

2025-11-23 13.55.18.gif

这样,立方体从圆台上掉下来可以跌落到地面上,这个地面还是必须的。

接下来创建物理世界:

image.png

首先指定重力加速度、创建地面的刚体。

import * as CANNON from 'cannon-es';

const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
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);

然后依次创建圆台、柱子的:

image.png

const pillarBody = new CANNON.Body({ mass: 0, position: new CANNON.Vec3(0, pillarHeight / 2, 0) });
pillarBody.addShape(new CANNON.Cylinder(pillarRadius, pillarRadius, pillarHeight, 16));
world.addBody(pillarBody);
const platformBody = new CANNON.Body({
    mass: 5,
    position: new CANNON.Vec3(0, platformY, 0)
});
platformBody.addShape(new CANNON.Cylinder(platformRadius, platformRadius, platformHeight, 32));
world.addBody(platformBody);

这俩都是圆柱体,形状一样,只是尺寸、位置不同。

然后给这俩圆柱添加铰链约束,让它们连到一起:

image.png

const hingeAxis = new CANNON.Vec3(0, 1, 0);
const hinge = new CANNON.HingeConstraint(pillarBody, platformBody, {
  pivotA: new CANNON.Vec3(0, pillarHeight / 2, 0),
  pivotB: new CANNON.Vec3(0, 0, 0),
  axisA: hingeAxis,
  axisB: hingeAxis
});
world.addConstraint(hinge);

这里主要是指定铰链连接的两端的位置,一个是圆台中心,是 0,0,0,另一个是中心点上面柱体一半的位置,也就是 0, h/2, 0

旋转都是绕 y 轴旋转。

然后,放上立方体:

image.png


const boxes = [];

function addBox() {
  const boxSize = 0.5;
  const angle = Math.random() * Math.PI * 2;
  const radius = Math.random() * (platformRadius - 0.5);
  const x = Math.cos(angle) * radius;
  const z = Math.sin(angle) * radius;
  const y = platformY + platformHeight / 2 + boxSize / 2 + 0.1;

  const boxGeo = new THREE.BoxGeometry(boxSize, boxSize, boxSize);
  const boxMat = new THREE.MeshPhongMaterial({
    color: new THREE.Color().setHSL(Math.random(), 0.8, 0.6)
  });
  const boxMesh = new THREE.Mesh(boxGeo, boxMat);
  boxMesh.position.set(x, y, z);
  group.add(boxMesh);

  const boxBody = new CANNON.Body({
    mass: 0.5,
    position: new CANNON.Vec3(x, y, z)
  });
  boxBody.addShape(new CANNON.Box(new CANNON.Vec3(boxSize / 2, boxSize / 2, boxSize / 2)));
  world.addBody(boxBody);

  boxes.push({ mesh: boxMesh, body: boxBody });
}

for (let i = 0; i < 5; i++) {
  addBox();
}

放了 5 个立方体,每个立方体都是随机的位置、颜色。

创建对应的刚体。

最后,更新下物理世界的物体,同步到 3D 场景:

image.png

function animate() {
    requestAnimationFrame(animate);
    world.step(1 / 60);
  
    platformMesh.position.copy(platformBody.position);
    platformMesh.quaternion.copy(platformBody.quaternion);
  
    boxes.forEach(box => {
      box.mesh.position.copy(box.body.position);
      box.mesh.quaternion.copy(box.body.quaternion);
    });  
}

animate();

接下来,给平台一个旋转速度:

image.png

platformBody.angularVelocity.y += 3;

2025-11-23 14.12.36.gif

可以看到,平台旋转会把立方体甩出去。

旋转的物理效果 cannon 已经内置了,我们只要给物体加上旋转速度 angularVelocity 就行。

案例代码上传了小册仓库

总结

这节我们实现了圆台旋转,把立方体甩出去。

这种向心力的物理效果不用自己实现,cannon 内置了,只要设置 angularVelocity 即可。

当然,如果旋转的物体和别的物体连在一起,同样可以通过铰链连接。

铰链、向心力,在很多涉及到旋转的场景里都会用到。

评论