Toggle navigation
集客麦麦@谢坤
首页
随笔
首页
>>
创作中心
>>
持续力(Force)...
持续力(Force)与冲量(Impulse)
在 `Cannon.js` 物理引擎中,涉及作用力的核心概念和方法可归纳如下: ### 1. 力(Force)与冲量(Impulse)的核心区别 - **力(Force)**: 持续作用于物体的力(单位:牛顿),会随时间累积改变物体的加速度(`F = ma`)。例如推箱子时持续施加的力,力的作用时间越长,物体获得的速度变化越大。 在代码中通过 `applyForce` 或 `applyLocalForce` 方法施加。 - **冲量(Impulse)**: 瞬间作用于物体的力的时间累积效应(单位:牛顿·秒),直接改变物体的动量(`Δp = m·Δv = 冲量`)。例如击球时球杆瞬间给予球的力,作用时间极短但效果显著。 在代码中通过 `applyImpulse` 或 `applyLocalImpulse` 方法施加。 ### 2. 四种作用力方法及坐标系差异 Cannon.js 中 `Body`(物理体)对象提供了四种施加作用力的方法,核心区别在于**坐标系(世界坐标系/局部坐标系)** 和**作用力类型(力/冲量)**: | 方法名 | 作用力类型 | 坐标系 | 说明 | |----------------------|------------|--------------|----------------------------------------------------------------------| | `applyForce` | 持续力 | 世界坐标系 | 对物体上的指定点(世界坐标系中的位置)施加持续力,力的方向基于世界坐标系(如全局x轴向右)。 | | `applyLocalForce` | 持续力 | 物体局部坐标系 | 对物体上的指定点(物体自身坐标系中的位置)施加持续力,力的方向随物体旋转而变化(如物体"前方")。 | | `applyImpulse` | 瞬间冲量 | 世界坐标系 | 对物体上的指定点(世界坐标系中的位置)施加瞬间冲量,冲量方向基于世界坐标系。 | | `applyLocalImpulse` | 瞬间冲量 | 物体局部坐标系 | 对物体上的指定点(物体自身坐标系中的位置)施加瞬间冲量,冲量方向随物体旋转而变化。 | ### 3. 关键参数说明 - **作用力向量(force/impulse)**: 三维向量(`x, y, z` 分量),控制作用力的大小和方向。例如 `new CANNON.Vec3(10, 0, 0)` 表示沿x轴正方向施加大小为10的力/冲量。 - **作用点(forcePoint)**: 三维向量(`x, y, z` 分量),指定力/冲量作用在物体上的位置。 - 若作用点与物体质心重合:物体仅平动,无旋转。 - 若作用点偏离质心:物体除平动外,还会因力矩产生旋转。 - **物理材质参数**: - `friction`(摩擦系数):影响物体接触时的摩擦力,值越高,运动中能量损耗越快(减速越明显)。 - `restitution`(弹性系数):影响物体碰撞后的反弹程度,值越高,反弹越强烈(能量损失越少)。 ### 4. 适用场景总结 - 若需模拟持续推动(如风吹动物体):用 `applyForce` 或 `applyLocalForce`。 - 若需模拟瞬间撞击(如球被击打):用 `applyImpulse` 或 `applyLocalImpulse`。 - 若作用力方向需随物体旋转而改变(如火箭推进器始终向前):用局部坐标系方法(`applyLocalForce`/`applyLocalImpulse`)。 - 若作用力方向固定(如重力、全局风力):用世界坐标系方法(`applyForce`/`applyImpulse`)。 通过代码中的 GUI 控制器,可实时调整力/冲量的大小、方向、作用点及物理材质参数,直观观察不同作用力对物体运动的影响。 ## 示例 ```js import * as THREE from 'three' import { OrbitControls } from 'three/addons/controls/OrbitControls.js' import * as CANNON from 'cannon-es' import CannonDebugger from 'cannon-es-debugger' import { addArrowAxesHelper } from '@/assets/utils/threeUtils' import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' import { GUI } from 'three/addons/libs/lil-gui.module.min.js' const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000, ) camera.position.set(0, 20, 40) camera.lookAt(0, 0, 0) const renderer = new THREE.WebGLRenderer() renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(renderer.domElement) // 创建一个平面作为地面 const groundGeometry = new THREE.PlaneGeometry(50, 50) const groundMaterial = new THREE.MeshBasicMaterial({ color: 0x999999, side: THREE.DoubleSide, }) const ground = new THREE.Mesh(groundGeometry, groundMaterial) ground.rotation.x = -Math.PI / 2 // 旋转平面使其水平 scene.add(ground) // 创建物理世界 const world = new CANNON.World() world.gravity.set(0, -9.82, 0) // 设置重力 // 添加物理调试器 const cannonDebugger = new CannonDebugger(scene, world, { color: 0x00ff00, // 调试器颜色 scale: 1, // 缩放比例 onInit: (body, obj3d, shape) => { // 参数为物理体、three.js网格和形状 console.log('物理体初始化', body, obj3d, shape) // 输出物理体初始化信息 }, }) cannonDebugger.update() // 更新调试器 // 创建 CANNON 地面 const groundBody = new CANNON.Body({ mass: 0, // 地面不受重力影响 position: new CANNON.Vec3(0, 0, 0), // 地面位置 shape: new CANNON.Plane(), // 使用平面形状 }) groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // 旋转平面使其水平 const groundBodyMaterial = new CANNON.Material({ name: 'groundMaterial', friction: 0.5, // 摩擦系数 restitution: 0.1, // 弹性系数 }) groundBody.material = groundBodyMaterial // 设置物理体材质 world.addBody(groundBody) // 将地面添加到物理世界 // 添加物理小球,点击施加作用力 const ballRadius = 1 const sphereBody = new CANNON.Body({ mass: 1, // 小球质量 position: new CANNON.Vec3(0, 10, 0), // 初始位置 shape: new CANNON.Sphere(ballRadius), // 使用球形状 }) sphereBody.material = new CANNON.Material({ name: 'sphereMaterial', friction: 0.1, // 摩擦系数 restitution: 0.8, // 弹性系数 }) // 防止高速小球穿透地面 // 为高速物体启用CCD sphereBody.ccdMotionThreshold = 0.001 // 运动阈值(超过此值启用CCD) sphereBody.ccdSweptSphereRadius = 0.02 // 用于CCD的 swept sphere 半径(小于物体半径) // 同时确保世界启用CCD world.defaultContactMaterial.contactEquationStiffness = 1e6 // 设置接触方程的刚度 world.defaultContactMaterial.contactEquationRelaxation = 3 // 设置接触方程的刚度和松弛度 world.addBody(sphereBody) // 将小球添加到物理世界 // three.js 小球 const sphereGeometry = new THREE.SphereGeometry(ballRadius, 32, 32) const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }) const sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial) sphereMesh.position.copy(sphereBody.position) // 设置小球位置 // 小球局部坐标辅助 const sphereAxisHelper = new THREE.AxesHelper(5) sphereMesh.add(sphereAxisHelper) // 添加坐标轴辅助 scene.add(sphereMesh) // 将小球添加到场景 // 添加物理护板 createBarrier([0, 5, -25], [0, 0, 0]) // 前方护板 createBarrier([0, 5, 25], [0, 0, 0]) // 后方护板 createBarrier([-25, 5, 0], [0, Math.PI / 2, 0]) // 左侧护板 createBarrier([25, 5, 0], [0, Math.PI / 2, 0]) // 右侧护板 // 上方护板 createBarrier([0, 10, 0], [Math.PI / 2, 0, 0], { width: 50, height: 50, depth: 0.2, }) function createBarrier(position, rotation, options = {}) { // width, height, depth const width = options.width || 50 const height = options.height || 10 const depth = options.depth || 0.2 // 默认深度为0.2 const barrierBody = new CANNON.Body({ mass: 0, // 静态护板 position: new CANNON.Vec3(...position), quaternion: new CANNON.Quaternion().setFromEuler(...rotation), // 设置旋转 shape: new CANNON.Box(new CANNON.Vec3(width / 2, height / 2, depth / 2)), // 使用盒形状 }) barrierBody.material = new CANNON.Material({ name: 'barrierMaterial', friction: 0.5, // 摩擦系数 restitution: 0.1, // 弹性系数 side: THREE.DoubleSide, // 双面渲染 }) // 设置护板材质 world.addBody(barrierBody) // 添加到物理世界 const barrierGeometry = new THREE.BoxGeometry(width, height, depth) const barrierMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, // 启用透明度 opacity: 0.1, // 半透明, 设置透明度,范围 [0, 1], 0 完全透明,1 完全不透明 }) const barrierMesh = new THREE.Mesh(barrierGeometry, barrierMaterial) barrierMesh.position.copy(barrierBody.position) // 设置位置 barrierMesh.quaternion.copy(barrierBody.quaternion) // 设置旋转 scene.add(barrierMesh) // 添加到场景 } // 持续作用力 const force = new CANNON.Vec3(10, 0, 0) // 向右施加的持续力 // 冲量作用力 const impulse = new CANNON.Vec3(10, 0, 0) // 向右施加的持续力 // 作用点 const forcePoint = new CANNON.Vec3(0, 0, 0) // 作用力点 // 选择作用力类型:force, localForce, impulse, localImpulse let selectForceType = 'force' // 点击屏幕触发作用力 renderer.domElement.addEventListener('click', () => { console.log('施加作用力', force, '和冲量', impulse) switch (selectForceType) { case 'force': sphereBody.applyForce(force, forcePoint) // 施加持续力 break case 'localForce': sphereBody.applyLocalForce(force, forcePoint) // 施加局部持续力 break case 'impulse': sphereBody.applyImpulse(impulse, forcePoint) // 施加冲量 break case 'localImpulse': sphereBody.applyLocalImpulse(impulse, forcePoint) // 施加局部冲量 break default: console.warn('未知的作用力类型:', selectForceType) } }) // GUI 切换作用力类型 const gui = new GUI() const forceTypeFolder = gui.addFolder('作用力') forceTypeFolder .add({ select: selectForceType }, 'select', [ 'force', 'localForce', 'impulse', 'localImpulse', ]) .name('作用力类型') .onChange((value) => { selectForceType = value // 更新选择的作用力类型 }) forceTypeFolder.add(force, 'x', -52, 52).name('持续力X') forceTypeFolder.add(force, 'y', -52, 52).name('持续力Y') forceTypeFolder.add(force, 'z', -52, 52).name('持续力Z') forceTypeFolder.add(impulse, 'x', -52, 52).name('冲量X') forceTypeFolder.add(impulse, 'y', -52, 52).name('冲量Y') forceTypeFolder.add(impulse, 'z', -52, 52).name('冲量Z') forceTypeFolder.add(forcePoint, 'x', -50, 50).name('作用点X') forceTypeFolder.add(forcePoint, 'y', -50, 50).name('作用点Y') forceTypeFolder.add(forcePoint, 'z', -50, 50).name('作用点Z') forceTypeFolder.open() // 打开作用力类型文件夹 const forceFolder = gui.addFolder('小球参数') forceFolder.add(sphereBody, 'mass', 0, 10).name('质量') forceFolder.add(sphereBody.material, 'restitution', 0, 1).name('弹性系数') // 弹力 forceFolder.add(sphereBody.material, 'friction', 0, 1).name('摩擦系数') // 摩擦力 forceFolder.open() // 打开小球参数文件夹 // 地面参数 const groundFolder = gui.addFolder('地面参数') // 地面摩擦力和弹性系数 groundFolder.add(groundBody.material, 'restitution', 0, 1).name('地面弹性系数') // 地面弹性系数 groundFolder.add(groundBody.material, 'friction', 0, 1).name('地面摩擦力') // 地面摩擦力 groundFolder.open() // 打开地面参数文件夹 function updatePhysics() { world.step(1 / 60) // 每帧更新物理世界 // 更新three.js网格位置和旋转 sphereMesh.position.copy(sphereBody.position) sphereMesh.quaternion.copy(sphereBody.quaternion) cannonDebugger.update() // 更新调试器 } // 辅助坐标轴 addArrowAxesHelper(scene, 10) // 添加相机控制器 const controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true // 启用阻尼效果 controls.dampingFactor = 0.1 // 阻尼系数 controls.enableZoom = true // 启用缩放 animate() function animate() { requestAnimationFrame(animate) updatePhysics() // 更新物理世界 controls.update() // 更新相机控制器 renderer.render(scene, camera) // 渲染场景 } ```