我们可以利用数学函数和一些解决方案像RayCaster来实现自己的物理效果,但是如果需求更加真实的物理效果,像是物体张力、摩擦力、拉伸、反弹等真实物理效果,最好使用外部库
我们会创建一个Three.js世界和一个Physics物理世界,虽然我们看不见后者但它是真实存在的,每当我们往Three.js世界添加对象时,相应的物理世界也会添加相同对象。物理世界在每一帧更新时都会相应更新到Three.js世界中。
例如物理世界中的球体在地板上进行真实弹跳效果时,我们会取其每一帧更新后的坐标并将坐标应用到Three.js世界中的对应球体。
我们需要决定要使用3D库还是2D库,因为有些时候一些3D交互可能被简化为2D,像打桌球,游泳等。比如这个网站
3D弹球
,弹球只在平面运动,而不会涉及到垂直方向上的弹跳。
Ammo.js
Cannon.js
Oimo.js
Matter.js
P2.js
Planck.js
Box2D.js
本次我们学习使用cannon.js库
npm i --save cannon
import CANNON from 'cannon'
// 实例化物理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)
然后我们创建带有质量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)
为更新物理世界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 - oldElapsedTime
oldElapsedTime = elapsedTime
world.step(1/60,deltaTime,3)
controls.update()
renderer.render(scene, camera)
window.requestAnimationFrame(tick)
虽然看上去没什么变化,但是实际上物理世界的球体sphereBody
一直在不停下落
console.log(sphereBody.position.y)

所以我们可以使用物理世界的球体坐标来更新Three.js世界中的球体坐标,设置之后会看到上图中的球体从(0,3,0)的高处下落穿过地面,因为物理世界中还没有添加地面
sphere.position.x = sphereBody.position.x
sphere.position.y = sphereBody.position.y
sphere.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)
观察上面动图发现当球体下落与地面碰撞后没有非常明显的反弹行为。
我们可以通过设置材质Material来更改摩擦和反弹行为。
- 先创建混凝土和塑料材质
const concreteMaterial = new CANNON.Material('concrete')
const plasticMaterial = new CANNON.Material('plastic')
- 创建联系材质ContactMaterial并通过
addContactMaterial
方法将其添加到world中
联系材质定义:两种材质相遇时发生的情况
ContactMaterial ( m1, m2 , [options] )
前两个参数是材质,第三个参数是一个包含碰撞属性的对象,如摩擦(摩擦多少)和恢复(反弹多少),两者的默认值均为0.3
const concretePlasticMaterial = new CANNON.ContactMaterial(
concreteMaterial,
plasticMaterial,
friction: 0.1,
restitution: 0.7,
world.addContactMaterial(concretePlasticMaterial)
然后给球体sphereBody
材质属性设置塑料材质
const sphereBody = new CANNON.Body({
......
material: plasticMaterial,
给地面floorBody
设置混凝土材质
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
属性,得到一样的结果
4.或者我们直接设置世界的默认联系材质defaultContactMaterial
属性,然后移除sphereBody和floorBody的material
属性
这样world中的所有材质就都是相同的默认材质。
这些操作通常取决于项目要求的真实程度
world.defaultContactMaterial = defaultContactMaterial
对刚体body
中的局部点施加力。
applyLocalForce ( force , localPoint )
force
—— 要应用的力向量(Vec3)
localPoint
—— body中要施加力的局部点(Vec3)
下面在球体中心原点处施加一个力(动画函数外部),在页面刷新完成那一帧施加力
sphereBody.applyLocalForce(new CANNON.Vec3(100,0,
0),new CANNON.Vec3(0,0,0))
在世界world
中的的局部点施加力,这个力会作用到刚体body
表面,例如风力
applyForce ( force , worldPoint )
force
—— 力的大小(Vec3)
worldPoint
—— 施加力的世界点(Vec3)
下面用applyForce
方法来模拟一股与球体运动反方向的持续的风。
因为要像风一样不断的持续施加力,所以回到动画函数,我们要在更新物理世界前更新每一帧动画。
sphereBody.applyForce(new CANNON.Vec3(-0.5,0,0),sphereBody.position)
world.step(1 / 60, deltaTime, 3)
分别移除物理世界和可视世界中的球体,还有动画函数中球体的设置。
const objectToUpdate = []
const createSphere = (radius,position) => {
const mesh = new THREE.Mesh(
new THREE.SphereBufferGeometry(radius,20,20),
new THREE.MeshStandardMaterial({
metalness: 0.3,
roughness: 0.4,
envMap: environmentMapTexture,
mesh.castShadow = true
mesh.position.copy(position)
scene.add(mesh)
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 - oldElapsedTime
oldElapsedTime = elapsedTime
world.step(1 / 60, deltaTime, 3)
for(const object of objectToUpdate) {
object.mesh.position.copy(object.body.position)
controls.update()
renderer.render(scene, camera)
window.requestAnimationFrame(tick)
添加一个createSphere按钮到GUI面板中,每点击一次按钮便生成一个球体。
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')
因为网格的几何体和材质都是一样的,所以我们可以将其移到外面,并把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) => {
const mesh = new THREE.Mesh(
sphereGeometry,
sphereMaterial
mesh.scale.set(radius,radius,radius)
mesh.castShadow = true
mesh.position.copy(position)
scene.add(mesh)
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,
传入的参数将是width,height,depth,position。
不过有一点需要注意,cannon.js中创建box与three.js创建box不一样。
在three.js中,创建几何体BoxBufferGeometry只需要直接提供立方体的宽高深就行,而在cannon.js中,它是根据立方体对角线距离的一半来计算生成形状,因此其宽高深必须乘以0.5
const createBox = (width,height,depth,position) => {
const mesh = new THREE.Mesh(boxGeometry,boxMaterial)
mesh.scale.set(width,height,depth)
mesh.castShadow = true
mesh.position.copy(position)
scene.add(mesh)
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)
cannon.js会一直测试物体是否与其他物体发生碰撞,这非常消耗CPU性能,这一步成为BroadPhase。当然我们可以选择不同的BroadPhase来更好的提升性能。
NaiveBroadphase(默认)
—— 测试所有的刚体相互间的碰撞。
GridBroadphase
—— 使用四边形栅格覆盖world,仅针对同一栅格或相邻栅格中的其他刚体进行碰撞测试。
SAPBroadphase(Sweep And Prune)
—— 在多个步骤的任意轴上测试刚体。
默认broadphase为NaiveBroadphase
,建议切换到SAPBroadphase
。
当然如果物体移动速度非常快,最后还是会产生一些bug。
切换到SAPBroadphase只需如下代码
world.broadphase = new CANNON.SAPBroadphase(world)
虽然我们使用改进的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 = 0
hitSound.play()
第二个问题就是当物体碰撞之后哪怕只是非常细微的碰撞,也会发出声音,这就导致声音异常繁多冗杂。为此,我们需要知道碰撞强度,如果碰撞强度不是很高,那我们将忽略其音效。
碰撞强度可以通过contact
属性中的getImpactVelocityAlongNormal()
方法获取到

因此我们只要当碰撞强度大于某个值时再触发音效就行了
const playHitSound = collision => {
const impactStrength = collision.contact.getImpactVelocityAlongNormal()
if (impactStrength > 1.5) {
hitSound.currentTime = 0
hitSound.play()
debugObject.reset = () => {
for (const object of objectToUpdate) {
object.body.removeEventListener('collide', playHitSound)
world.removeBody(object.body)
scene.remove(object.mesh)
gui.add(debugObject,'reset')
由于JavaScript是单线程模型,即所有任务只能在同一个线程上面完成,前面的任务没有做完,后面的就只能等待,这对于日益增强的计算能力来说不是一件好事。所以在HTML5中引入了Web Worker的概念,来为JavaScript创建多线程环境,将其中一些任务分配给Web Worker运行,二者可以同时运行,互不干扰。Web Worker是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。
在计算机中做物理运算的是CPU,负责WebGL图形渲染的是GPU。现在我们的所有事情都是在CPU中的同一个线程完成的,所以该线程可能很快就过载,而解决方案就是使用worker
。
我们通常把进行物理计算的部分放到worker里面,具体可看这个例子的源码web worker example
cannon.js已经有四年没维护,但还是有些人在其基础上更新并修复细节。
安装并引入
npm i --save [email protected]
import * as CANNON from 'cannon-es'
https://github.com/pmndrs/cannon-es
我们可以利用数学函数和一些解决方案来实现自己的物理效果,但是如果需求更加真实的物理效果,像是物体张力、摩擦力、拉伸、反弹等真实物理效果,最好使用外部库原理我们会创建一个Three.js世界和一个Physics物理世界,虽然我们看不见后者但它是真实存在的,每当我们往Three.js世界添加对象时,相应的物理世界也会添加相同对象。物理世界在每一帧更新时都会相应更新到Three.js世界中。例如物理世界中的球体在地板上进行真实弹跳效果时,我们会取其每一帧更新后的坐标并将坐标应用到Three.js世界中的对应
github地址:https://github.com/hua1995116/Fly-Three.js
大家好,我是秋风。继上一篇《Three.js系列: 游戏中的第一/三人称视角》今天想要和大家分享的呢,是做一个海洋球池。
海洋球大家都见过吧?就是商场里非常受小孩子们青睐的小球,自己看了也想往里蹦跶的那种。
就想着做一个海洋球池,然后顺便带大家来学习学习 Three.js 中的物理引擎。
那么让我们开始吧,要实现一个海洋球池,那么首先肯定得有“球”吧。
因此先带大家来实现一个小球,而恰恰在
为cannon-es-debugger对您的“三场景”对象和Cannon物理物体的引用:
import { Scene } from 'three'
import { World } from 'cannon-es'
import cannonDebugger from 'cannon-es-debugger'
const scene = new Scene ( )
const world = new World ( )
cannonDebugger ( scene , world . bodies , opt
本篇将介绍的基础使用,并用与写一个简单的demo
在使用Cannon.js时通常会与其它3d库同时使用,因为Cannon.js就和后端差不多只负责数据 3d库则负责展示效果。我们这里将使用
three.js来进行演示
首先我们需要下载Cannon.js 我们可以直接下载js文件或者使用CDN也可以使用npm直接安装相关示例
2.由于Cannonjs
物理引擎库他只是负责数据而不负责显示,这时我们需要使用其它3d库来进行数据效果的显示
相关代码:
最后我们将Cannonjs代码与
three.js代码合并
一般使用Physijs创建物理效果
配置Physijs,需要设置这两个属性
Physijs.scripts.worker = '../libs/physijs_worker.js';
Physijs.scripts.ammo = '../libs/ammo.js';
一般用法:
var scene = new Physijs.Scene();//创建场景
scene.setGravity(new...
前一篇介绍了物理引擎Physijs,场景中的对象只需要用特定的Physijs对象封装一下便可以在场景中展现物理效果(重力、碰撞检测等)。但Physijs并没有为模型的加载提供一种特定的方法。所以由3Dmax或是Blender等建模软件创建的模型通过传统方式加载到场景中并没有物理效果。
为了使模型具有物理效果这里我使用了一种方法(才疏学浅,仅供参考)。
这里使用的是r71的加载方式,最新的有所不
Demo地址:nsytsqdtn.github.io/demo/sprite…
three.js的粒子系统
three.js的粒子系统主要是依靠精灵体来创建的,要实现
three.js中的粒子系统创建,一般有两种方式。
第一种是在场景中使用很多歌
THREE.Sprite创建单个的精灵,这样创...
【Three.js】七、three.js粒子、精灵、点云一、粒子(Sprite)二、点云(Points)
粒子(也叫精灵),可用来创建非常细小的物体,可以用来模拟雨、雪、烟等其他有趣的效果。
一、粒子(Sprite)
使用Three.Sprite()可以用来创建一个粒子,该构造函数接受一个材质参数。下面是一个利用Three.Sprite()创建的一个粒子矩阵。
let createSpr...
你想
学习关于
three.js 的内容吗?我可以为你提供一些相关的信息和资源。首先,
three.js 是一个用于创建 3D 图形的
JavaScript 库,它可以在 Web 浏览器中渲染复杂的三维场景和动画效果。如果你想
学习如何使用
three.js,以下是一些步骤和资源供你参考:
1. 了解基础知识:在开始
学习 three.js 之前,建议你先掌握 HTML、CSS 和
JavaScript 的基础知识。
2. 安装和设置:你可以通过下载
three.js 的最新版本文件或使用 CDN 来引入
three.js 库。确保在你的 HTML 文件中正确地链接和配置
three.js。
3.
学习文档和示例:
three.js 官方网站提供了详细的文档和示例,你可以通过阅读文档和尝试示例来了解
three.js 的各种功能和用法。官方文档地址是:https://
threejs.org/docs/index.html
4.
学习基本概念:熟悉
three.js 中的基本概念,如场景(Scene)、相机(Camera)、渲染器(Renderer)、几何体(Geometry)、材质(Material)等。理解这些概念对于构建三维场景至关重要。
5. 创建简单场景:从简单的场景开始,逐步添加和调整对象、光照和材质等。通过实践来熟悉
three.js 的基本用法和 API。
6.
学习进阶技术:一旦你掌握了基本的用法,你可以
学习更高级的技术,如动画、纹理映射、阴影、粒子效果等。
three.js 提供了丰富的功能和扩展库,你可以根据自己的需求进一步探索。
除了官方文档外,还有一些优秀的教程和资源可供参考,例如:
-
three.js Fundamentals:https://
threejsfundamentals.org/
Yale Qi:
three.js学习笔记(十三)——真实渲染
Yale Qi:
three.js学习笔记(十三)——真实渲染
Yale Qi:
three.js学习笔记(九)——光线投射
Yale Qi: