上节学了铰链,但并没有体现出向心力来,这节我们来做一个能体现向心力的案例:
一个旋转的转盘,上面一堆立方体,随着旋转,立方体会被甩出去。
接下来实现下:
创建项目:
npx create-vite cannon-hingle-platform

进入项目,安装依赖:
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


然后下面加上柱子:

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);

还有下面的地面:

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);

这样,立方体从圆台上掉下来可以跌落到地面上,这个地面还是必须的。
接下来创建物理世界:

首先指定重力加速度、创建地面的刚体。
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);
然后依次创建圆台、柱子的:

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);
这俩都是圆柱体,形状一样,只是尺寸、位置不同。
然后给这俩圆柱添加铰链约束,让它们连到一起:

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 轴旋转。
然后,放上立方体:

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 场景:

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();
接下来,给平台一个旋转速度:

platformBody.angularVelocity.y += 3;

可以看到,平台旋转会把立方体甩出去。
旋转的物理效果 cannon 已经内置了,我们只要给物体加上旋转速度 angularVelocity 就行。
案例代码上传了小册仓库
总结
这节我们实现了圆台旋转,把立方体甩出去。
这种向心力的物理效果不用自己实现,cannon 内置了,只要设置 angularVelocity 即可。
当然,如果旋转的物体和别的物体连在一起,同样可以通过铰链连接。
铰链、向心力,在很多涉及到旋转的场景里都会用到。