本文目录
- 前言
- 最终效果
- 1、postStep
- 2、前置准备
- 2.1 代码
- 2.2 效果
- 3、removeConstraint
- 3.1 解除约束代码
- 效果
- 4、完整代码
前言
在3D物理引擎的广阔天地中,
cannon-es
以其轻量级、高性能和易于集成的特点,成为了WebGL
环境中物理模拟的首选工具。它不仅能够精准地模拟刚体碰撞、力学行为和约束等物理现象,还提供了丰富的API供开发者自由调用。
本文将深入探讨cannon-es
中的两个关键特性:postStep
事件(尽管cannon-es
官方文档中可能并未直接提及,但我们可以基于物理引擎的通用概念进行阐述)和removeConstraint
方法。postStep
通常被理解为在物理模拟步骤之后执行的操作,它允许开发者在物理世界更新后插入自定义逻辑。而removeConstraint
方法则是用来从物理世界中移除不再需要的约束,从而优化模拟性能和准确性。
接下来,我们将通过前置代码准备、解除约束的代码示例及其效果展示,以及完整的代码汇总,来详细解析这两个特性的实际应用。无论你是物理引擎的新手,还是经验丰富的开发者,本文都将为你提供一份详尽的指南,帮助你更好地掌握cannon-es
的精髓。
最终效果
1、postStep
Cannon-es.js
作为Cannon.js
的现代分支,是一款专为WebGL
设计的轻量级、高性能3D物理引擎,能够轻松模拟真实世界中的物理现象,如刚体碰撞、力学模拟和约束等。在物理引擎中,模拟步骤之后执行一些额外的逻辑,比如更新渲染场景、处理用户输入、计算新的物理状态等。即在物理模拟步骤之后执行的操作。postStep
事件是在每个物理时间步后被调用的事件。这个事件可以用于执行一些与物理更新后的状态更改相关的操作。所以我们可以监听在物体约束中被力冲击后断开约束。接下来将用代码演示。
2、前置准备
2.1 代码
<template>
<canvas ref="cannonDemo" class="cannonDemo">
</canvas>
</template>
<script setup>
import { onMounted, ref } from "vue"
import * as THREE from 'three'
import * as CANNON from 'cannon-es'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
const cannonDemo = ref('null')
onMounted(() => {
const cannonDemoDomWidth = cannonDemo.value.offsetWidth
const cannonDemoDomHeight = cannonDemo.value.offsetHeight
// 创建场景
const scene = new THREE.Scene
// 创建相机
const camera = new THREE.PerspectiveCamera( // 透视相机
45, // 视角 角度数
cannonDemoDomWidth / cannonDemoDomHeight, // 宽高比 占据屏幕
0.1, // 近平面(相机最近能看到物体)
1000, // 远平面(相机最远能看到物体)
)
camera.position.set(0, 2, 70)
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true, // 抗锯齿
canvas: cannonDemo.value
})
// 设置设备像素比
renderer.setPixelRatio(window.devicePixelRatio)
// 设置画布尺寸
renderer.setSize(cannonDemoDomWidth, cannonDemoDomHeight)
const light = new THREE.AmbientLight(0x404040, 200); // 柔和的白光
scene.add(light);
let meshes = []
let phyMeshes = []
const physicsWorld = new CANNON.World()
// 设置y轴重力
physicsWorld.gravity.set(0, -9.82, 0)
const planeShape = new CANNON.Box(new CANNON.Vec3(10, 0.05, 10))
const planeBody = new CANNON.Body({
shape: planeShape,
position: new CANNON.Vec3(0,0,0)
})
physicsWorld.addBody(planeBody)
phyMeshes.push(planeBody)
const planeGeometry = new THREE.BoxGeometry(20, 0.1, 20)
const planeMaterial = new THREE.MeshBasicMaterial({wireframe: true})
const planeMesh = new THREE.Mesh(
planeGeometry,
planeMaterial
)
scene.add(planeMesh)
meshes.push(planeMesh)
const sphereShape = new CANNON.Sphere(1)
const sphereGeometry = new THREE.SphereGeometry(1, 16, 16)
const sphereMaterial = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true})
let previousBody
for(let i = 0; i < 10; i++) {
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 25 - i * 2.2 , 0),
mass: i == 0 ? 0 : 1
})
physicsWorld.addBody(sphereBody)
phyMeshes.push(sphereBody)
const sphereMesh = new THREE.Mesh(
sphereGeometry,
sphereMaterial
)
scene.add(sphereMesh)
meshes.push(sphereMesh)
if (i > 0) {
const constraint = new CANNON.DistanceConstraint(
previousBody,
sphereBody,
2.2
)
physicsWorld.addConstraint(constraint)
}
previousBody = sphereBody
}
const sphereClickShape = new CANNON.Sphere(0.8)
const sphereClickGeometry = new THREE.SphereGeometry(0.8, 16, 16)
const sphereClickMaterial = new THREE.MeshBasicMaterial({color: 0x0000ff})
window.addEventListener('click', () => {
const sphereClickBody = new CANNON.Body({
mass: 1,
shape: sphereClickShape,
position: new CANNON.Vec3(5, 15, 0)
})
physicsWorld.addBody(sphereClickBody)
phyMeshes.push(sphereClickBody)
sphereClickBody.velocity.set(-10, 0, 0)
const sphereClickMesh = new THREE.Mesh(sphereClickGeometry, sphereClickMaterial)
scene.add(sphereClickMesh)
meshes.push(sphereClickMesh)
})
const axesHelper = new THREE.AxesHelper(30);
scene.add(axesHelper);
const updatePhysic = () => { // 因为这是实时更新的,所以需要放到渲染循环动画animate函数中
physicsWorld.step(1 / 60)
for (let i = 0; i < phyMeshes.length; i++) {
meshes[i].position.copy(phyMeshes[i].position)
meshes[i].quaternion.copy(phyMeshes[i].quaternion)
}
}
// 控制器
const control = new OrbitControls(camera, renderer.domElement)
// 开启阻尼惯性,默认值为0.05
control.enableDamping = true
// 渲染循环动画
function animate() {
// 在这里我们创建了一个使渲染器能够在每次屏幕刷新时对场景进行绘制的循环(在大多数屏幕上,刷新率一般是60次/秒)
requestAnimationFrame(animate)
updatePhysic()
// 更新控制器。如果没在动画里加上,那必须在摄像机的变换发生任何手动改变后调用
control.update()
renderer.render(scene, camera)
}
// 执行动画
animate()
})
</script>
<style scoped>
.cannonDemo {
width: 100vw;
height: 100vh;
}
</style>
2.2 效果
可以看到我们就有点击生成蓝色小球冲击的效果。
3、removeConstraint
在cannon-es
中,约束(Constraints
)是用来限制或控制物体运动的一种机制。通过约束,可以将物体连接在一起,或者限制物体在某些方向上的运动。而removeConstraint
方法,则是用来从物理世界中移除已经添加的约束的。
3.1 解除约束代码
physicsWorld.addEventListener('postStep', () => {
for(let i = 0; i < physicsWorld.constraints.length; i++) {
const constraint = physicsWorld.constraints[i]
// 获取约束力度的绝对值大小
let multiplier = Math.abs(constraint.equations[0].multiplier)
console.log(multiplier)
if (multiplier > 1000) {
// 约束破坏
physicsWorld.removeConstraint(constraint)
}
}
})
效果
可以看到我前面3次由于力度不够所以约束不能被破坏,第4下的力直接把约束破坏了。
4、完整代码
最后给出完整代码:
<template>
<canvas ref="cannonDemo" class="cannonDemo">
</canvas>
</template>
<script setup>
import { onMounted, ref } from "vue"
import * as THREE from 'three'
import * as CANNON from 'cannon-es'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
const cannonDemo = ref('null')
onMounted(() => {
const cannonDemoDomWidth = cannonDemo.value.offsetWidth
const cannonDemoDomHeight = cannonDemo.value.offsetHeight
// 创建场景
const scene = new THREE.Scene
// 创建相机
const camera = new THREE.PerspectiveCamera( // 透视相机
45, // 视角 角度数
cannonDemoDomWidth / cannonDemoDomHeight, // 宽高比 占据屏幕
0.1, // 近平面(相机最近能看到物体)
1000, // 远平面(相机最远能看到物体)
)
camera.position.set(0, 2, 70)
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true, // 抗锯齿
canvas: cannonDemo.value
})
// 设置设备像素比
renderer.setPixelRatio(window.devicePixelRatio)
// 设置画布尺寸
renderer.setSize(cannonDemoDomWidth, cannonDemoDomHeight)
const light = new THREE.AmbientLight(0x404040, 200); // 柔和的白光
scene.add(light);
let meshes = []
let phyMeshes = []
const physicsWorld = new CANNON.World()
// 设置y轴重力
physicsWorld.gravity.set(0, -9.82, 0)
const planeShape = new CANNON.Box(new CANNON.Vec3(10, 0.05, 10))
const planeBody = new CANNON.Body({
shape: planeShape,
position: new CANNON.Vec3(0,0,0)
})
physicsWorld.addBody(planeBody)
phyMeshes.push(planeBody)
const planeGeometry = new THREE.BoxGeometry(20, 0.1, 20)
const planeMaterial = new THREE.MeshBasicMaterial({wireframe: true})
const planeMesh = new THREE.Mesh(
planeGeometry,
planeMaterial
)
scene.add(planeMesh)
meshes.push(planeMesh)
const sphereShape = new CANNON.Sphere(1)
const sphereGeometry = new THREE.SphereGeometry(1, 16, 16)
const sphereMaterial = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true})
let previousBody
for(let i = 0; i < 10; i++) {
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 25 - i * 2.2 , 0),
mass: i == 0 ? 0 : 1
})
physicsWorld.addBody(sphereBody)
phyMeshes.push(sphereBody)
const sphereMesh = new THREE.Mesh(
sphereGeometry,
sphereMaterial
)
scene.add(sphereMesh)
meshes.push(sphereMesh)
if (i > 0) {
const constraint = new CANNON.DistanceConstraint(
previousBody,
sphereBody,
2.2
)
physicsWorld.addConstraint(constraint)
}
previousBody = sphereBody
}
const sphereClickShape = new CANNON.Sphere(0.8)
const sphereClickGeometry = new THREE.SphereGeometry(0.8, 16, 16)
const sphereClickMaterial = new THREE.MeshBasicMaterial({color: 0x0000ff})
let muscle = 0
window.addEventListener('click', () => {
const sphereClickBody = new CANNON.Body({
mass: 1,
shape: sphereClickShape,
position: new CANNON.Vec3(5, 15, 0)
})
physicsWorld.addBody(sphereClickBody)
phyMeshes.push(sphereClickBody)
muscle++
sphereClickBody.velocity.set(-10, 0, 0)
if (muscle > 3) {
sphereClickBody.velocity.set(-50, 0, 0)
}
const sphereClickMesh = new THREE.Mesh(sphereClickGeometry, sphereClickMaterial)
scene.add(sphereClickMesh)
meshes.push(sphereClickMesh)
})
physicsWorld.addEventListener('postStep', () => {
for(let i = 0; i < physicsWorld.constraints.length; i++) {
const constraint = physicsWorld.constraints[i]
// 获取约束力度的绝对值大小
let multiplier = Math.abs(constraint.equations[0].multiplier)
console.log(multiplier)
if (multiplier > 1000) {
// 约束破坏
physicsWorld.removeConstraint(constraint)
}
}
})
const axesHelper = new THREE.AxesHelper(30);
scene.add(axesHelper);
const updatePhysic = () => { // 因为这是实时更新的,所以需要放到渲染循环动画animate函数中
physicsWorld.step(1 / 60)
for (let i = 0; i < phyMeshes.length; i++) {
meshes[i].position.copy(phyMeshes[i].position)
meshes[i].quaternion.copy(phyMeshes[i].quaternion)
}
}
// 控制器
const control = new OrbitControls(camera, renderer.domElement)
// 开启阻尼惯性,默认值为0.05
control.enableDamping = true
// 渲染循环动画
function animate() {
// 在这里我们创建了一个使渲染器能够在每次屏幕刷新时对场景进行绘制的循环(在大多数屏幕上,刷新率一般是60次/秒)
requestAnimationFrame(animate)
updatePhysic()
// 更新控制器。如果没在动画里加上,那必须在摄像机的变换发生任何手动改变后调用
control.update()
renderer.render(scene, camera)
}
// 执行动画
animate()
})
</script>
<style scoped>
.cannonDemo {
width: 100vw;
height: 100vh;
}
</style>
在学习的路上,如果你觉得本文对你有所帮助的话,那就请关注点赞评论三连吧,谢谢,你的肯定是我写博的另一个支持。