Cannon.js是一个开源的3D物理引擎,用于在WebGL中创建3D物理模拟。它提供了一个灵活的API,可以应用于许多WebGL场景。
我们需要了解一些基本概念,包括物理实体、碰撞、物理变换、物理世界和物理实体之间的约束等。
官网:https://pmndrs.github.io/cannon-es/
npm: https://www.npmjs.com/package/cannon-es
npm i --save cannon-es
cannon-es是一个轻量级且易于使用的网络 3D 物理引擎。它的灵感来自three.js的简单 API,并基于ammo.js和Bullet 物理引擎。
首先要设置的是我们的物理世界,它将容纳我们所有的物理实体并推动模拟向前发展。
让我们用地球引力创造一个世界。请注意,cannon.js 使用SI 单位(米、千克、秒等)。
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.82, 0), // m/s²
})
为了推进模拟,我们必须调用world.fixedStep()每一帧。作为第一个参数,我们可以传递我们希望模拟运行的固定时间步长,默认值是1 / 60meaning 60fps。 world.fixedStep()跟踪上次调用它以独立于帧率保持相同速度的模拟,因为requestAnimationFrame调用可能因不同设备而异,或者如果存在性能问题。在此处阅读有关固定模拟步进的更多信息。
function animate() {
requestAnimationFrame(animate)
// Run the simulation independently of framerate every 1 / 60 ms
world.fixedStep()
}
// Start the simulation loop
animate()
如果你想打发自上次通话以来的时间(dt在游戏世界中),你可以使用更高级的world.step().
查看高级世界步进示例
const timeStep = 1 / 60 // seconds
let lastCallTime
function animate() {
requestAnimationFrame(animate)
const time = performance.now() / 1000 // seconds
if (!lastCallTime) {
world.step(timeStep)
} else {
const dt = time - lastCallTime
world.step(timeStep, dt)
}
lastCallTime = time
}
// Start the simulation loop
animate()
刚体是将在世界中模拟的实体,它们可以是简单的形状,如Sphere、Box、Plane、Cylinder,或更复杂的形状,如ConvexPolyhedron、Particle、Heightfield、Trimesh。
让我们创建一个基本的球体。
const radius = 1 // m
const sphereBody = new CANNON.Body({
mass: 5, // kg
shape: new CANNON.Sphere(radius),
})
sphereBody.position.set(0, 10, 0) // m
world.addBody(sphereBody)
如您所见,我们指定了一个质量属性,质量定义了身体在受力影响时的行为。
当物体具有质量并受到力的影响时,它们被称为动态物体。还有不受力影响但可以具有速度并四处移动的运动学实体。第三种类型的物体是静态物体,它们只能在世界中定位并且不受力或速度的影响。
如果将质量为 0 的物体传递给物体,则该物体会自动标记为静态物体。您还可以在主体选项中明确主体类型。例如,让我们创建一个静态地面。
const groundBody = new CANNON.Body({
type: CANNON.Body.STATIC,
shape: new CANNON.Plane(),
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // make it face up
world.addBody(groundBody)
以下是所有以前的片段组合在一个完整的示例中。
import * as CANNON from 'cannon-es'
// Setup our physics world
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.82, 0), // m/s²
})
// Create a sphere body
const radius = 1 // m
const sphereBody = new CANNON.Body({
mass: 5, // kg
shape: new CANNON.Sphere(radius),
})
sphereBody.position.set(0, 10, 0) // m
world.addBody(sphereBody)
// Create a static plane for the ground
const groundBody = new CANNON.Body({
type: CANNON.Body.STATIC, // can also be achieved by setting the mass to 0
shape: new CANNON.Plane(),
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // make it face up
world.addBody(groundBody)
// Start the simulation loop
function animate() {
requestAnimationFrame(animate)
world.fixedStep()
// the sphere y position shows the sphere falling
console.log(`Sphere y position: ${sphereBody.position.y}`)
}
animate()
请注意,cannon 不负责将任何内容渲染到屏幕上,它只是计算模拟的数学。要在屏幕上实际显示某些内容,您必须使用渲染库,例如three.js。让我们看看如何实现这一目标。
首先,你必须在 three.js 中创建 body 的对应实体。例如,这里是如何在 three.js 中创建一个球体。
const radius = 1 // m
const geometry = new THREE.SphereGeometry(radius)
const material = new THREE.MeshNormalMaterial()
const sphereMesh = new THREE.Mesh(geometry, material)
scene.add(sphereMesh)
然后,您必须将 three.js 网格与 cannon.js 主体连接起来。要做到这一点,您需要在走过世界后每帧将位置和旋转数据从身体复制到网格。
function animate() {
requestAnimationFrame(animate)
// world stepping...
sphereMesh.position.copy(sphereBody.position)
sphereMesh.quaternion.copy(sphereBody.quaternion)
// three.js render...
}
animate()
一个物理引擎,一边渲染引擎,渲染引擎从物理引擎中获取数据进行渲染
const world = new CANNON.World(); // 创建物理世界
world.gravity.set(0, -9.8, 0); // 设置重力方向
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);
//设置物体材质
const sphereWorldMaterial = new CANNON.Material();
// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});
// 将物体添加至物理世界
world.addBody(sphereBody);
物理引擎如何与渲染引擎关联
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime); // 更新
sphere.position.copy(sphereBody.position); // 渲染引擎复制物理引擎中的数据 做渲染 自由落体
自由落体
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";
// 目标:使用cannon引擎
console.log(CANNON);
// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);
// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);
// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true; // 阴影
scene.add(sphere);
const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);
floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;// 接收阴影
scene.add(floor);
// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);
//设置物体材质
const sphereWorldMaterial = new CANNON.Material();
// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});
// 将物体添加至物理世界
world.addBody(sphereBody);
//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true; // 阴影
scene.add(dirLight);
// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);
// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();
function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime); // 更新
sphere.position.copy(sphereBody.position); // 渲染引擎复制物理引擎中的数据
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
小球如何撞到地面之后停下来
那么也要创建一个物理世界地面
// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";
// 目标:使用cannon引擎
console.log(CANNON);
// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);
// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);
// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true;
scene.add(sphere);
const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);
floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);
//设置物体材质
const sphereWorldMaterial = new CANNON.Material();
// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});
// 将物体添加至物理世界
world.addBody(sphereBody);
// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);
//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true;
scene.add(dirLight);
// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);
// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();
function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime);
sphere.position.copy(sphereBody.position);
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength); // 获取碰撞的强度
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.play();
}
}
sphereBody.addEventListener("collide", HitEvent);
免费音乐素材下载地址,爱给网:https://www.aigei.com/
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";
// 目标:使用cannon引擎
console.log(CANNON);
// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);
// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);
// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true;
scene.add(sphere);
const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);
floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);
//设置物体材质
const sphereWorldMaterial = new CANNON.Material("sphere");
// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});
// 将物体添加至物理世界
world.addBody(sphereBody);
// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength); // 获取碰撞的强度
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.play();
}
}
sphereBody.addEventListener("collide", HitEvent);
// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);
// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
sphereMaterial,
floorMaterial,
{
// 摩擦力
friction: 0.1,
// 弹性
restitution: 0.7,
}
);
// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);
//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true;
scene.add(dirLight);
// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);
// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();
function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime);
sphere.position.copy(sphereBody.position);
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");
// 设置地面材质
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
cubeWorldMaterial,
floorMaterial,
{
// 摩擦力
friction: 0.1,
// 弹性
restitution: 0.7,
}
);
// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);
// 设置世界碰撞的默认材料,如果材料没有设置,都用这个
world.defaultContactMaterial = defaultContactMaterial;
window.addEventListener("click", createCube);
const cubeArr = [];
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");
function createCube() {
// 创建立方体和平面
const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial();
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.castShadow = true; //阴影
scene.add(cube);
// 创建物理cube形状
const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));
// 创建物理世界的物体
const cubeBody = new CANNON.Body({
shape: cubeShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: cubeWorldMaterial,
});
cubeBody.applyLocalForce(
new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
);
// 将物体添加至物理世界
world.addBody(cubeBody);
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength);
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.volume = impactStrength / 12;
hitSound.play();
}
}
cubeBody.addEventListener("collide", HitEvent);
cubeArr.push({
mesh: cube,
body: cubeBody,
});
}
// 下落后物体旋转
cubeArr.forEach((item) => {
item.mesh.position.copy(item.body.position);
// 设置渲染的物体跟随物理的物体旋转
item.mesh.quaternion.copy(item.body.quaternion);
});
撞击后声音逐渐减弱
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength);
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.volume = impactStrength / 12;
hitSound.play();
}
}
cubeBody.addEventListener("collide", HitEvent);
cubeBody.applyLocalForce(
new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
);
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";
// 目标:设置cube跟着旋转
console.log(CANNON);
// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);
// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);
const cubeArr = [];
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");
function createCube() {
// 创建立方体和平面
const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial();
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.castShadow = true; //阴影
scene.add(cube);
// 创建物理cube形状
const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));
// 创建物理世界的物体
const cubeBody = new CANNON.Body({
shape: cubeShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: cubeWorldMaterial,
});
cubeBody.applyLocalForce(
new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
);
// 将物体添加至物理世界
world.addBody(cubeBody);
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength);
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.volume = impactStrength / 12;
hitSound.play();
}
}
cubeBody.addEventListener("collide", HitEvent);
cubeArr.push({
mesh: cube,
body: cubeBody,
});
}
window.addEventListener("click", createCube);
// 平面
const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);
floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true; // 接收阴影
scene.add(floor);
// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0); // 重力方向
// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");
// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
// 设置地面材质
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);
// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
cubeWorldMaterial,
floorMaterial,
{
// 摩擦力
friction: 0.1,
// 弹性
restitution: 0.7,
}
);
// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);
// 设置世界碰撞的默认材料,如果材料没有设置,都用这个
world.defaultContactMaterial = defaultContactMaterial;
//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true; // 投放阴影
scene.add(dirLight);
// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);
// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();
function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime); // 更新
// cube.position.copy(cubeBody.position);
cubeArr.forEach((item) => {
item.mesh.position.copy(item.body.position);
// 设置渲染的物体跟随物理的物体旋转
item.mesh.quaternion.copy(item.body.quaternion);
});
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
流体模拟是一种用于模拟流体运动和形态的技术,常用于计算机图形学、动画、物理学、流体力学等领域。
在计算机图形学中,流体模拟可以用来创建各种真实的水、火、烟、云等效果。如海浪、河流、火焰、烟雾、雨雪等。
在动画制作中,流体模拟可以用来制作角色的衣服、头发、发型等效果,还可以用来制作特效动画,如爆炸、火焰等。
在物理学和流体力学中,流体模拟可以用来研究流体运动的规律,如水流、气流、火焰等,对于工程设计和模拟有很大的帮助。
在游戏开发中,流体模拟可以用来制作游戏中的水、火、烟等效果,让游戏画面更加真实。
流体模拟技术还可以应用于医学影像、纺织工业、液压机械、化学工程等其他领域。
SPH (Smooth Particle Hydrodynamics) 是一种流体模拟算法,它可以模拟流体的运动和形态。SPH算法的核心思想是将流体分成许多粒子,然后用这些粒子来模拟流体的运动。
SPH算法的主要步骤如下:
SPH算法还有很多优化和变种,如模拟不同流体(如气体和液体)、多相流体、多孔介质、粘性流体、非牛顿流体等等。
需要注意的是,在实际应用中,SPH算法需要解决的问题非常复杂,需要对数学和物理有很好的理解。如果不熟悉这些知识,可能需要学习一些基础知识。
Cannon.js是一个开源的物理引擎,它支持WebGL和JavaScript。Cannon.js中的SPHSystem类是用来模拟流体的一个类,它使用了SPH算法。
使用Cannon.js中的SPHSystem类,可以很容易地在WebGL中模拟流体。下面是一个简单的例子,说明了如何使用SPHSystem类:
// 创建一个SPH系统
var sph = new CANNON.SPHSystem();
// 添加一些粒子
for (var i = 0; i < 100; i++) {
var p = new CANNON.SPHSystem.Particle();
p.position.set(Math.random(), Math.random(), Math.random());
sph.addParticle(p);
}
// 每一帧更新粒子状态
function update() {
sph.step();
requestAnimationFrame(update);
}
update();
这样就可以在WebGL中模拟流体。需要注意的是,还需要使用WebGL来渲染粒子,并且需要设置相应的参数才能达到期望的效果。
CANNON.SPHSystem 还支持设置流体的密度、粘性、阻力等参数,还支持给粒子施加力,添加障碍物等等。需要根据实际需求来设置相应的参数。
CANNON.SPHSystem类中有一些属性来控制流体的表现,这四个属性分别是:
● density : 流体的密度,它用来控制流体的粘度和阻力。
● particles : 包含所有粒子的数组。
● smoothingRadius : 用来控制粒子之间交互的距离,粒子之间的距离小于这个值时,它们之间会产生影响。
● viscosity : 流体的粘度,它用来控制流体的阻力。
其中 density 和 viscosity 是控制流体行为的重要参数。密度越高,流体就会越粘稠,阻力也会越大。粘度越高,流体就会越阻力,移动越困难。
particles 属性是粒子数组,可以通过它访问到粒子的属性,如位置、速度等。
smoothingRadius 是交互半径,这个值越大,粒子之间的交互就越广,越小,粒子之间的交互就越窄,根据实际需求来调整。
如果想要模拟出真实的流体效果,需要根据实际场景来调整这些参数。
RaycastVehicle
RaycastVehicle是cannon.js提供了一个车辆对象。并且官方给我们提供了demo。
何为Raycast
为何称为RaycastVehicle呢?这与该对象的物理模拟原理有关系。该对象使用刚体(CANNON.Body)作为车身,从刚体的四个角处向下发射固定长度的射线,射线与地面的交叉点作为车辆与地面的接触点,在该点为车身刚体施加纵向的悬挂弹力与横向的牵引摩擦力。
使用方法可以参考官方示例。
1、首先构造一个RaycastVehicle对象:
vehicle = new CANNON.RaycastVehicle({
chassisBody: chassisBody,
indexRightAxis: 0,
indexForwardAxis: 2,
indexUpAxis: 1,
});
vehicle.addToWorld(world);
其中,chassisBody是代表车身的刚体,indexRightAxis、indexForwardAxis、indexUpAxis官方示例中并没有用到,他们分别代表车的右、前、上轴,0、1、2分别代表x、y、z轴。
2、添加逻辑车轮:
vehicle.addWheel(options);
options对象为车轮及悬挂的参数:
chassisConnectionPointLocal: new Vec3(),// 车轮连接点,相对于chassisBody(也是发射射线的起点)
directionLocal: new Vec3(),// 车轮的下方方向(垂直车身向下)
axleLocal: new Vec3(),// 车轴方向
suspensionRestLength: 1,// 悬挂长度(未受任何力)
suspensionMaxLength: 2,// 悬挂最大长度,限制计算出的suspensionLength
suspensionStiffness: 100,// 悬挂刚度
dampingCompression: 10,// 悬挂压缩阻尼
dampingRelaxation: 10,// 悬挂复原阻尼
maxSuspensionForce: Number.MAX_VALUE, // 限制计算出的suspensionForce
maxSuspensionTravel: 1,// 悬挂可伸长或压缩的最大距离
radius: 1,// 车轮半径
frictionSlip: 10000,// 滑动摩檫系数(用于计算车轮所能提供的最大摩檫力)
rollInfluence: 0.01,// 施加侧向力时的位置系数,越小越接近车身,防止侧翻
添加车轮碰撞。添加好逻辑车轮后,还要在world中添加刚体以产生碰撞:
在cannon-es的RaycastVehicle类中,设置轮子的对象包含以下属性:
for (var i = 0; i < vehicle.wheelInfos.length; i++) {
var wheel = vehicle.wheelInfos[i];
var cylinderShape = new CANNON.Cylinder(wheel.radius, wheel.radius, wheel.radius / 2, 20);
var wheelBody = new CANNON.Body({
mass: 0
});
wheelBody.type = CANNON.Body.KINEMATIC;
wheelBody.collisionFilterGroup = 0; // turn off collisions
var q = new CANNON.Quaternion();
q.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), Math.PI / 2);// 把竖着的圆柱体放倒作为车轮
wheelBody.addShape(cylinderShape, new CANNON.Vec3(), q);
wheelBodies.push(wheelBody);
world.addBody(wheelBody);
对于车轮碰撞,还要实时更新其Transom:
world.addEventListener('postStep', function () {
for (var i = 0; i < vehicle.wheelInfos.length; i++) {
var t = vehicle.wheelInfos[i].worldTransform;
var wheelBody = wheelBodies[i];
wheelBody.position.copy(t.position);
wheelBody.quaternion.copy(t.quaternion);
}
});
车辆控制。可以使用RaycastVehicle的如下函数控制车辆:
applyEngineForce = function(value, wheelIndex) // 施加牵引力
setSteeringValue = function(value, wheelIndex) // 设置转向角(弧度)
setBrake = function(brake, wheelIndex) // 刹车
实现动画。在每帧渲染之前,把车身、车轮的Transom拷贝给three.js或Bayalon.js图形,就实现了动画.
在赛车游戏中,漂移能够很大程度上增加游戏的娱乐性。要理解漂移,首先要了解汽车的转向。
后轮无滑移转向时,前轮与后轮的瞬心即为转向中心.
无滑移状态下,转向中心静止不变,车辆将沿转向中心做圆周运动。当漂移时,转向中心也做圆周运动,车辆运动的圆周比无滑移转向时的半径小:
实现漂移,可以在用户按下漂移键后,修改后轮的摩擦系数:
vehicle.wheelInfos[2].frictionSlip= up ? 3.5: 1.5;
vehicle.wheelInfos[3].frictionSlip= up ? 3.5: 1.5;
但是,此时很容易出现漂移过度,车子打一个圈:
前文中介绍了转向半径,这里再利用一下。设转向半径为r,轴距为l,轮距为w:
根据阿克曼条件,两轮的转向角为:
r = 6 + Math.abs(vehicle.currentVehicleSpeedKmHour) / 10
switch (event.keyCode) {
// 。。。
case 39: // right
vehicle.setSteeringValue(up ? 0 : -Math.atan(2 / (r + 1 / 2)), 0);
vehicle.setSteeringValue(up ? 0 : -Math.atan(2 / (r - 1 / 2)), 1);
break;
case 37: // left
vehicle.setSteeringValue(up ? 0 : Math.atan(2 / (r - 1 / 2)), 0);
vehicle.setSteeringValue(up ? 0 : Math.atan(2 / (r + 1 / 2)), 1);
break;
case 67:
vehicle.wheelInfos[2].frictionSlip = up ? 3.5 : 1.4;
vehicle.wheelInfos[3].frictionSlip = up ? 3.5 : 1.4;
此外,在卡丁车类游戏中,也可以使用参考文献[1]中,给车身施加侧向力的方法让车辆产生更顺滑的漂移