Appearance
CannonJS 物理引擎
引用
js
import * as CANNON from "cannon-es";
模拟乒乓球下落反弹
一、碰撞体Body
通过CANNON.Body
类,可以创建一个用于物体物理模拟计算,比如用 Body 表示一个球、一个箱子、一张桌子,你可以计算 Body 的位置、速度等物理量
创建
jsconst body = new CANNON.Body();
设置
Body
的物理属性jsconst sphereMaterial = new CANNON.Material() const body = new CANNON.Body({ mass: 0.3,// 碰撞体质量0.3kg material: sphereMaterial,//碰撞体材质 // position: new CANNON.Vec3(0, 100, 0), shape: new CANNON.Sphere(1);, //1m半径球体 }); body.position.y = 100; // 碰撞体的三维空间中位置
二、创建物理世界
创建并定义物理属性
jsconst world = new CANNON.World(); // 设置物理世界重力加速度 world.gravity.set(0, -9.8, 0); //单位:m/s²
.addBody()
把物体添加到物理世界jsconst world = new CANNON.World(); world.addBody(body);
world.step()
物理世界更新计算js//固定的时间步长1/60秒 const fixedTimeStep = 1 / 60; function render() { world.step(fixedTimeStep); requestAnimationFrame(render); }
三、创建物理地面
js
// 物理地面
const groundMaterial = new CANNON.Material();
const groundBody = new CANNON.Body({
mass: 0, // 质量为0,始终保持静止,不会受到力碰撞或加速度影响
shape: new CANNON.Plane(),
material: groundMaterial,
});
// 改变平面默认的方向,法线默认沿着z轴,旋转到平面向上朝着y方向
//旋转规律类似threejs 平面
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);
四、设置碰撞物体反弹系数
restitution 的范围一般是 0~1, 值越大弹性越大
js
const contactMaterial = new CANNON.ContactMaterial(groundMaterial, sphereMaterial, {
restitution: 0.7, //反弹恢复系数
});
// 把关联的材质添加到物理世界中
world.addContactMaterial(contactMaterial);
五、构建 three 网格模型
js
// 网格小球
const geometry = new THREE.SphereGeometry(1.5);
const material = new THREE.MeshLambertMaterial({
color: 0xffff00,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.y = 100;
// 网格地面
const planeGeometry = new THREE.PlaneGeometry(200, 200);
const texture = new THREE.TextureLoader().load("./瓷砖.jpg");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(3, 3);
const planeMaterial = new THREE.MeshLambertMaterial({
color: 0x777777,
map: texture,
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
planeMesh.rotateX(-Math.PI / 2);
六、同步网格小球和物理小球位置属性
js
const clock = new THREE.Clock();
// 渲染循环
function render() {
world.step(1 / 60); //更新物理计算
mesh.position.copy(body.position); // 网格小球与物理小球位置同步
renderer.render(scene, camera);
requestAnimationFrame(render);
}
七、控制小球重复下落
点击按钮,body 回到下落的初始位置
js
let addBool = false; //标记body是否已经添加到world中
document.getElementById("test").addEventListener("click", function () {
body.position.y = 1; //点击按钮,body回到下落的初始位置
if (!addBool) {
world.addBody(body); //添加到物理世界,才开始下落
addBool = true;
}
});
碰撞事件声音
碰撞体Body
的碰撞事件collide
js
body.addEventListener("collide", (event) => {
console.log("碰撞信息", event.contact);
});
碰撞事件的接触属性.contact
当两个碰撞体 Body 碰撞的时候,会生成碰撞信息,碰撞事件.contact
包含内容是 body 之间的碰撞信息
- contact.bi: 监控碰撞事件的 Body
- contact.bj: 与 contact.bi 发生的碰撞的 Body
contact.getImpactVelocityAlongNormal()
两个物体在碰撞法线方向上对的相对速度
js
const contact = event.contact;
//获得沿法线的冲击速度
contact.getImpactVelocityAlongNormal();
实现
加载音频代码
jsconst audio = new Audio("./碰撞声.wav"); document.getElementById("audio").addEventListener("click", function () { audio.volume = 0; //按钮开启声音时候,设置静音 audio.play(); });
设置碰撞事件触发音频播放
jsbody.addEventListener("collide", () => { audio.volume = 1; audio.play(); });
可以用碰撞相对速度表示碰撞程度,通过碰撞相对速度,控制音量大小
js
body.addEventListener("collide", (event) => {
const contact = event.contact;
const ImpactV = contact.getImpactVelocityAlongNormal();
// 碰撞越狠,声音越大
//4.5比ImpactV最大值吕略微大一点,这样音量范围0~1
audio.volume = ImpactV / 4.5;
audio.play();
});
长方体 Box 碰撞体(箱子下落)
创建物理长方体
三维向量的三个参数分别是表示长方体 xyz 方向的一半尺寸
jsconst boxMaterial = new CANNON.Material(); const body = new CANNON.Body({ mass: 0.3, material: boxMaterial, //碰撞体材质 // x、y、z三个方向的尺寸(长宽高),分别为1.0、0.4、0.6 shape: new CANNON.Box(new CANNON.Vec3(0.5, 0.2, 0.3)), }); body.position.y = 5;
创建网格长方体
js// 网格长方体 const geometry = new THREE.BoxGeometry(1.0, 0.4, 0.6); const material = new THREE.MeshLambertMaterial({ map: new THREE.TextureLoader().load("./箱子.jpg"), }); const mesh = new THREE.Mesh(geometry, material); mesh.position.y = 5;
设置弹性系数
jsnew CANNON.ContactMaterial(groundMaterial, boxMaterial, { restitution: 0.2, //箱子,反弹恢复系数低一点 });
其余物理地面、物理世界等设置同乒乓球反弹
旋转长方体
箱子换个姿态角度下落,观察下落效果差异
- 网格箱子旋转
js
// 设置箱子下落的初始姿态角度
mesh.rotation.set(Math.PI / 3, Math.PI / 3, Math.PI / 3);
- 箱子碰撞体 body 旋转
js
body.quaternion.setFromEuler(Math.PI / 3, Math.PI / 3, Math.PI / 3);
- 同步箱子 Mesh 和 Body 姿态角度
.quaternion
渲染循环中不仅要同步位置.position
,也要同步两者的姿态角度.quaternion
(四元数)
js
// 渲染循环
function render() {
world.step(1 / 60);
// 网格mesh与boyd位置同步
mesh.position.copy(body.position);
//同步姿态角度
mesh.quaternion.copy(body.quaternion);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
加载外部 gltf 模型
思路基本一样, 模型尺寸通过包围盒Box3()
获取, 再赋值给物理模型
js
const box3 = new THREE.Box3();
box3.expandByObject(mesh); //计算模型包围盒
const size = new THREE.Vector3();
box3.getSize(size); //包围盒计算箱子的尺寸
物理箱子:
js
// 物理箱子
const body = new CANNON.Body({
//...
shape: new CANNON.Box(new CANNON.Vec3(size.x / 2, size.y / 2, size.z / 2)),
});
凸多面体ConvexPolyhedron
ConvexPolyhedron
可以用来表示不同外形的凸多面体
加载 gltf 模型凸多面体
jsconst gltf = await loader.loadAsync("../凸多面体.glb"); const mesh = gltf.scene.getObjectByName("多面体"); //获取凸多面体网格模型 mesh.position.y = 5; console.log("mesh.geometry", mesh.geometry);
获取模型三角形顶点数据
jsconst vertices = []; //所有三角形顶点位置数据 const faces = []; //所有三角形面的索引值 const pos = mesh.geometry.attributes.position; for (let i = 0; i < pos.count; i++) { const x = pos.getX(i); const y = pos.getY(i); const z = pos.getZ(i); vertices.push(new CANNON.Vec3(x, y, z)); } const index = mesh.geometry.index.array; for (let i = 0; i < index.length; i += 3) { const a = index[i]; const b = index[i + 1]; const c = index[i + 2]; faces.push([a, b, c]); }
创建凸多面体并赋予顶点数据
js// CannonJS的凸多面体ConvexPolyhedron const shape = new CANNON.ConvexPolyhedron({ vertices, faces }); // 物理凸多面体 const body = new CANNON.Body({ shape: shape, });