three.js学习笔记(十)——物理引擎
我们可以利用数学函数和一些解决方案像RayCaster来实现自己的物理效果,但是如果需求更加真实的物理效果,像是物体张力、摩擦力、拉伸、反弹等真实物理效果,最好使用外部库
原理
我们会创建一个Three.js世界和一个Physics物理世界,虽然我们看不见后者但它是真实存在的,每当我们往Three.js世界添加对象时,相应的物理世界也会添加相同对象。物理世界在每一帧更新时都会相应更新到Three.js世界中。
例如物理世界中的球体在地板上进行真实弹跳效果时,我们会取其每一帧更新后的坐标并将坐标应用到Three.js世界中的对应球体。
库
我们需要决定要使用3D库还是2D库,因为有些时候一些3D交互可能被简化为2D,像打桌球,游泳等。比如这个网站3D弹球,弹球只在平面运动,而不会涉及到垂直方向上的弹跳。
3D库
Ammo.js
Cannon.js
Oimo.js
2D库
Matter.js
P2.js
Planck.js
Box2D.js
Cannon.js
本次我们学习使用cannon.js库
安装并引入
npm i --save cannon
import CANNON from 'cannon'
初始场景
通过cannon.js创建物理世界
// 实例化物理world
const world = new CANNON.World()
往world中通过gravity
属性添加重力,为三维向量Vec3(Cannon.js的Vec3等价于Three.js的Vector3)
world.gravity.set(0,-9.82,0)
在物理世界中添加球状刚体
在Three.js中我们是通过Mesh创建物体,在Cannon.js中则是通过Body创建刚体,这些刚体Bodies可以坠落并且能其他物体进行碰撞。
但首先,创造刚体Body
前得先有一个形状shape
,就像我们之前创造网格Mesh前得先有几何体geometry
//创建球形
const sphereShape = new CANNON.Sphere(0.5) //半径0.5,与Three.js世界中的球体半径相同
然后我们创建带有质量mass
与初始位置position
的球状刚体,类似Three.js创建网格。
关于质量mass
属性,如果俩个物体进行碰撞,质量小的那个更容易被撞开,可以想象现实情况。
//创建物理世界中的球体
const sphereBody = new CANNON.Body({//质量mass:1,//位置position:new CANNON.Vec3(0,3,0),shape:sphereShape,
})
通过addBody()
方法将球状刚体添加到world
中
world.addBody(sphereBody)
更新物理世界和Three.js场景
为更新物理世界world
,我们必须使用时间步长step(...)
。
关于时间步长原理可以看这篇文章fix_your_timestep
step ( dt , [timeSinceLastCalled] , [maxSubSteps=10] )
dt
:固定时间戳(要使用的固定时间步长)
[timeSinceLastCalled]
:自上次调用函数以来经过的时间
[maxSubSteps=10]
:每个函数调用可执行的最大固定步骤数
回到动画函数,我们希望以60fps的速度运行所以第一个参数设置为 1/60;对于第二个参数,我们需要计算自上一帧以来经过了多少时间,通过将前一帧的elapsedTime减去当前elapsedTime来获得,不要去使用Clock类中的getDelta()方法
/*** Animate*/
const clock = new THREE.Clock()
let oldElapsedTime = 0
const tick = () =>
{const elapsedTime = clock.getElapsedTime()const deltaTime = elapsedTime - oldElapsedTimeoldElapsedTime = elapsedTime//更新物理世界world.step(1/60,deltaTime,3)// Update controlscontrols.update()// Renderrenderer.render(scene, camera)// Call tick again on the next framewindow.requestAnimationFrame(tick)
}
虽然看上去没什么变化,但是实际上物理世界的球体sphereBody
一直在不停下落
console.log(sphereBody.position.y)
所以我们可以使用物理世界的球体坐标来更新Three.js世界中的球体坐标,设置之后会看到上图中的球体从(0,3,0)的高处下落穿过地面,因为物理世界中还没有添加地面
//更新Three.js世界球体的坐标sphere.position.x = sphereBody.position.xsphere.position.y = sphereBody.position.ysphere.position.z = sphereBody.position.z//或者使用下面代码(等价于上面)sphere.position.copy(sphereBody.position)
添加平面到物理世界
设置地面质量mass
为0,表面这个body是静态的
注:我们可以创建一个由多种形状shape
组成的刚体body
//创建物理世界地面形状
const floorShape = new CANNON.Plane()
//创建物理世界地面
const floorBody = new CANNON.Body()
floorBody.mass = 0
floorBody.addShape(floorShape)
world.addBody(floorBody)
由于平面初始化是是竖立着的,所以需要将其旋转至跟Three.js平面一样。
在cannon.js中,我们只能使用四元数(Quaternion)来旋转,可以通过setFromAxisAngle(…)
方法,第一个参数是旋转轴,第二个参数是角度
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1,0,0),Math.PI*0.5)
联系材质ContactMaterial
观察上面动图发现当球体下落与地面碰撞后没有非常明显的反弹行为。
我们可以通过设置材质Material来更改摩擦和反弹行为。
//创建混凝土材质
const concreteMaterial = new CANNON.Material('concrete')
//创建塑料材质
const plasticMaterial = new CANNON.Material('plastic')
ContactMaterial ( m1, m2 , [options] )
前两个参数是材质,第三个参数是一个包含碰撞属性的对象,如摩擦(摩擦多少)和恢复(反弹多少),两者的默认值均为0.3
//创建联系材质
const concretePlasticMaterial = new CANNON.ContactMaterial(concreteMaterial,plasticMaterial,{friction: 0.1,restitution: 0.7,}
)
//添加联系材质
world.addContactMaterial(concretePlasticMaterial)
//创建物理世界球体
const sphereBody = new CANNON.Body({......//塑料材质material: plasticMaterial,
})
floorBody.material = concreteMaterial
//创建默认材质
const defaultMaterial = new CANNON.Material('default')
//创建默认联系材质
const defaultContactMaterial = new CANNON.ContactMaterial(defaultMaterial,defaultMaterial,{friction:0.1,restitution: 0.7,}
)
//把默认联系材质添加到世界中
world.addContactMaterial(defaultContactMaterial)
然后修改sphereBody和floorBody的material
属性,得到一样的结果
world.defaultContactMaterial = defaultContactMaterial
施加外力
applyLocalForce
对刚体body
中的局部点施加力。
applyLocalForce ( force , localPoint )
force
—— 要应用的力向量(Vec3)
localPoint
—— body中要施加力的局部点(Vec3)
下面在球体中心原点处施加一个力(动画函数外部),在页面刷新完成那一帧施加力
sphereBody.applyLocalForce(new CANNON.Vec3(100,0,0),new CANNON.Vec3(0,0,0))
applyForce
在世界world
中的的局部点施加力,这个力会作用到刚体body
表面,例如风力
applyForce ( force , worldPoint )
force
—— 力的大小(Vec3)
worldPoint
—— 施加力的世界点(Vec3)
下面用applyForce
方法来模拟一股与球体运动反方向的持续的风。
因为要像风一样不断的持续施加力,所以回到动画函数,我们要在更新物理世界前更新每一帧动画。
//风力大小0.5方向反向,施力的世界点位置与球状刚体位置一致,
sphereBody.applyForce(new CANNON.Vec3(-0.5,0,0),sphereBody.position)
//Update physics world
world.step(1 / 60, deltaTime, 3)
处理多个对象
分别移除物理世界和可视世界中的球体,还有动画函数中球体的设置。
1.创建createSphere函数生成球体
//创建用以保存更新网格和刚体对象的数组
const objectToUpdate = []
//创建生成球体函数
const createSphere = (radius,position) => {//Three.js 网格const mesh = new THREE.Mesh(new THREE.SphereBufferGeometry(radius,20,20),new THREE.MeshStandardMaterial({metalness: 0.3,roughness: 0.4,envMap: environmentMapTexture,}))mesh.castShadow = truemesh.position.copy(position)scene.add(mesh)//Cannon.js 刚体const shape = new CANNON.Sphere(radius)const body = new CANNON.Body({mass:1,position:new CANNON.Vec3(0,3,0),shape,material:defaultMaterial})body.position.copy(position)world.addBody(body)//保存对象更新数组中objectToUpdate.push({mesh:mesh,body:body})
}
之后在动画函数中,在更新物理世界后更新网格位置
const clock = new THREE.Clock()
let oldElapsedTime = 0
const tick = () => {const elapsedTime = clock.getElapsedTime()const deltaTime = elapsedTime - oldElapsedTimeoldElapsedTime = elapsedTime//Update physics worldworld.step(1 / 60, deltaTime, 3)for(const object of objectToUpdate) {object.mesh.position.copy(object.body.position)}// Update controlscontrols.update()// Renderrenderer.render(scene, camera)// Call tick again on the next framewindow.requestAnimationFrame(tick)
}
2.添加至DAT.GUI
添加一个createSphere按钮到GUI面板中,每点击一次按钮便生成一个球体。
//先创建对象来存储createSphere函数
//因为gui.add()第一个参数必须是一个对象,第二个参数是对象的一个属性
const debugObject = {}
debugObject.createSphere = () => {createSphere(Math.random() * 0.5, {x: (Math.random() - 0.5) * 3,y: 3,z: (Math.random() - 0.5) * 3,})
}
gui.add(debugObject,'createSphere')
3.优化函数
因为网格的几何体和材质都是一样的,所以我们可以将其移到外面,并把sphereGeometry的半径设为 1 ,然后在函数中将网格根据半径参数进行缩放
//创建用以保存更新网格和刚体对象的数组
const objectToUpdate = []
const sphereGeometry = new THREE.SphereBufferGeometry(1,20,20)
const sphereMaterial = new THREE.MeshStandardMaterial({metalness: 0.3,roughness: 0.4,envMap:environmentMapTexture
})
const createSphere = (radius, position) => {//Three.js 网格const mesh = new THREE.Mesh(sphereGeometry,sphereMaterial)mesh.scale.set(radius,radius,radius)mesh.castShadow = truemesh.position.copy(position)scene.add(mesh)//Cannon.js 刚体const shape = new CANNON.Sphere(radius)const body = new CANNON.Body({mass: 1,position: new CANNON.Vec3(0, 3, 0),shape,material: defaultMaterial,})body.position.copy(position)world.addBody(body)//保存对象更新数组中objectToUpdate.push({mesh: mesh,body: body,})
}
4.同样步骤创建createBox函数生成立方体
传入的参数将是width,height,depth,position。
不过有一点需要注意,cannon.js中创建box与three.js创建box不一样。
在three.js中,创建几何体BoxBufferGeometry只需要直接提供立方体的宽高深就行,而在cannon.js中,它是根据立方体对角线距离的一半来计算生成形状,因此其宽高深必须乘以0.5
const createBox = (width,height,depth,position) => {//Three.js 网格const mesh = new THREE.Mesh(boxGeometry,boxMaterial)mesh.scale.set(width,height,depth)mesh.castShadow = truemesh.position.copy(position)scene.add(mesh)//Cannon.js 刚体const shape = new CANNON.Box(new CANNON.Vec3(width * 0.5,height * 0.5,depth * 0.5))const body = new CANNON.Body({mass: 1,position: new CANNON.Vec3(0, 3, 0),shape,material: defaultMaterial,})body.position.copy(position)world.addBody(body)//保存对象更新数组中objectToUpdate.push({mesh: mesh,body: body,})
}
debugObject.createBox = () => {createBox(Math.random(), Math.random(), Math.random(), {x: (Math.random() - 0.5) * 3,y: 3,z: (Math.random() - 0.5) * 3,})
}gui.add(debugObject,'createBox')
可以看到效果有点违和,因为立方体是被弹开而不是倒下。为此我们将在动画函数中修改代码,把刚体的quaternion复制给网格的quaternion。
关于四元数quaternion,用以表示对象局部旋转。
for (const object of objectToUpdate) {object.mesh.position.copy(object.body.position)object.mesh.quaternion.copy(object.body.quaternion)}
碰撞检测性能优化
1.粗测阶段(BroadPhase)
cannon.js会一直测试物体是否与其他物体发生碰撞,这非常消耗CPU性能,这一步成为BroadPhase。当然我们可以选择不同的BroadPhase来更好的提升性能。
NaiveBroadphase(默认)
—— 测试所有的刚体相互间的碰撞。
GridBroadphase
—— 使用四边形栅格覆盖world,仅针对同一栅格或相邻栅格中的其他刚体进行碰撞测试。
SAPBroadphase(Sweep And Prune)
—— 在多个步骤的任意轴上测试刚体。
默认broadphase为NaiveBroadphase
,建议切换到SAPBroadphase
。
当然如果物体移动速度非常快,最后还是会产生一些bug。
切换到SAPBroadphase只需如下代码
world.broadphase = new CANNON.SAPBroadphase(world)
2.睡眠Sleep
虽然我们使用改进的BroadPhase算法,但所有物体还是都要经过测试,即便是那些不再移动的刚体。
因此我们需要当刚体移动非常非常缓慢以至于看不出其有在移动时,我们说这个刚体进入睡眠,除非有一股力施加在刚体上来唤醒它使其开始移动,否则我们不会进行测试。
只需以下一行代码即可
world.allowSleep = true
当然我们也可以通过Body的sleepSpeedLimit
属性或sleepTimeLimit
属性来设置刚体进入睡眠模式的条件。
sleepSpeedLimit
——如果速度小于此值,则刚体被视为进入睡眠状态。
sleepTimeLimit
—— 如果刚体在这几秒钟内一直处于沉睡,则视为处于睡眠状态。
事件
我们可以监听刚体事件像是碰撞colide
、睡眠sleep
或唤醒wakeup
下面我们给刚体碰撞事件添加音效。
注意:有些浏览器(如Chrome)会阻止播放声音,除非用户与页面进行交互(像点击任意地方)
创建音效并添加到立方体碰撞事件中
const hitSound = new Audio('/sounds/hit.mp3')
//播放音效
const playHitSound = ()=>{hitSound.play()
}
const createBox = (width,height,depth,position) => {......body.position.copy(position)//碰撞事件body.addEventListener('collide',playHitSound)world.addBody(body)......
}
之后你会发现音效是有了,但是很违和,因为碰撞音效非常规律,明显与实际不符。这是因为当声音在播放的时候我们再调用hitSound.play()
是不会发生任何事情的,因为声音已经是在播放状态了。为此我们需要使用currentTime
属性将声音重置为重头开始播放
const playHitSound = ()=>{hitSound.currentTime = 0hitSound.play()
}
第二个问题就是当物体碰撞之后哪怕只是非常细微的碰撞,也会发出声音,这就导致声音异常繁多冗杂。为此,我们需要知道碰撞强度,如果碰撞强度不是很高,那我们将忽略其音效。
碰撞强度可以通过contact
属性中的getImpactVelocityAlongNormal()
方法获取到
因此我们只要当碰撞强度大于某个值时再触发音效就行了
const playHitSound = collision => {const impactStrength = collision.contact.getImpactVelocityAlongNormal()if (impactStrength > 1.5) {hitSound.currentTime = 0hitSound.play()}
}
重置场景
//重置场景
debugObject.reset = () => {for (const object of objectToUpdate) {//移除刚体bodyobject.body.removeEventListener('collide', playHitSound)world.removeBody(object.body)// 移除网格meshscene.remove(object.mesh)}
}gui.add(debugObject,'reset')
其他
Web Worker
由于JavaScript是单线程模型,即所有任务只能在同一个线程上面完成,前面的任务没有做完,后面的就只能等待,这对于日益增强的计算能力来说不是一件好事。所以在HTML5中引入了Web Worker的概念,来为JavaScript创建多线程环境,将其中一些任务分配给Web Worker运行,二者可以同时运行,互不干扰。Web Worker是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。
在计算机中做物理运算的是CPU,负责WebGL图形渲染的是GPU。现在我们的所有事情都是在CPU中的同一个线程完成的,所以该线程可能很快就过载,而解决方案就是使用worker
。
我们通常把进行物理计算的部分放到worker里面,具体可看这个例子的源码web worker example
cannon-es
cannon.js已经有四年没维护,但还是有些人在其基础上更新并修复细节。
安装并引入
npm i --save cannon-es@0.15.1
import * as CANNON from 'cannon-es'
https://github.com/pmndrs/cannon-es
three.js学习笔记(十)——物理引擎相关推荐
- three.js学习笔记(十八)——调整材质
介绍 到现在为止,我们都在创建新的着色器材质,但是如果我们想要修改一个Three.js内置的材质呢?或许我们对MeshStandardMaterial的处理结果感到满意,但是希望往里边添加顶点动画. ...
- Vue.js 学习笔记 十二 Vue发起Ajax请求
首先需要导入vue-resource.js,可以自己下载引入,也可以通过Nuget下载,它依赖于Vue.js. 全局使用方式: Vue.http.get(url,[options]).then(suc ...
- Vue.js 学习笔记 十 自定义按键事件
<div id="divApp"><!--任何键盘动作都会触发--><input type="text" v-on:keyup=& ...
- JS学习笔记十——时间常用方法
前言 时间本身是 JS 中的一个数据类型 Date,是一种引用数据类型,其创建方式有两种:一是 new Date(),创建时间对象,且为当前终端的时间,即电脑时间:二是 new Date(年,月,日, ...
- three.js学习笔记(十二)——使用Blender自定义模型
这次我们将学习如何用3D软件创建自己的模型 选择软件 有很多软件如Cinema 4D.Maya.3DS Max.Blender.ZBrush.Marmoset Toolbag.Substance Pa ...
- Vue.js 学习笔记十二:Vue CLI 之创建一个项目
目录 创建一个项目 创建一个项目 运行以下命令来创建一个新项目: vue create vuecli-demo 你会被提示选取一个 preset.你可以选默认的包含了基本的 Babel + ESLin ...
- three.js学习笔记(十九)——后期处理
介绍 后期处理是指在最终图像(渲染)上添加效果.人们大多在电影制作中使用这种技术,但我们也可以在WebGL中使用. 后期处理可以是略微改善图像或者产生巨大影响效果. 下面链接是Three.js官方文档 ...
- three.js学习笔记(十六)——汹涌的海洋
介绍 现在我们知道了如何使用着色器并绘制一些图案,那么这次就要用它来创建一个汹涌的海洋. 我们将使用调试面板来设置波浪的动画并保持对各项参数的控制. 初始场景 现在,我们只有一个使用MeshBasic ...
- Vue.js 学习笔记 十一 自定义指令
之前看到过v-bind,v-on等指令,Vue还可以自定义指<div id="divApp" <div v-focus></div> & ...
最新文章
- php网页弹出图片,商城网站是如何单击头像直接弹出可以上传图片然后预览?
- linux系统怎么样同步时间,Linux系统时间同步
- myelicpse无法连接mysql_myeclipse连接不到mysql
- SQL Server 2008 R2 事务与隔离级别实例讲解
- Markdown编辑器对比分析
- MATLAB代码:全面ADMM算法代码,实现了三种ADMM迭代方式 参考文档:《基于串行和并行ADMM算法的电_气能量流分布式协同优化_瞿小斌》
- 银耳椰椰——冲刺计划
- 第一代操盘手图解“庄家操盘五部曲”;股市不可不知的赚钱法则!
- IRP IO_STACK_LOCATION 《寒江独钓》内核学习笔记(1)
- 【家庭药箱系列】布洛芬小史
- Python爬虫入门-python之jieba库制作词云图
- 淘宝旺旺号转userid 或 uid 接口与方法
- mydumper 介绍及使用
- 台灯显色指数多少合适?专家教你护眼灯怎么选
- Linux LCD 驱动
- jquery遍历得到的 Map 数据,
- mysql查询,inner join有多条符合条件的只取其中一条即可
- SMETA验厂辅导,提出过不合规项的所有部分,应对以下内容有清楚的解释
- 【标准全文】GB 38031-2020 电动汽车用动力蓄电池安全要求
- python实现cv2图片读取显示及图片不显示或显示不全的问题分析
热门文章
- ios wallet开发_Wallet app-想知道钱都花哪去了?试试它吧#iOS #Android
- Winform 串口通讯之地磅
- WebUpload 视频上传,支持多视频上传
- SSL_2293【暗黑游戏】
- 在Sqlite中实现RowNumber功能
- adsp21489 CCES修改kernel file使得SPI启动速度加快
- cannot create temp file for here-document: No space left on device解决方案
- FFmpeg 音视频解封装
- ssm通用数据展示系统 毕业设计-附源码200934
- Efficient Shapelet Discovery for Time Series Classification(TKDE)