我们可以利用数学函数和一些解决方案像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

本次我们学习使用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 - oldElapsedTime oldElapsedTime = elapsedTime //更新物理世界 world.step(1/60,deltaTime,3) // Update controls controls.update() // Render renderer.render(scene, camera) // Call tick again on the next frame window.requestAnimationFrame(tick)

虽然看上去没什么变化,但是实际上物理世界的球体sphereBody一直在不停下落

console.log(sphereBody.position.y)

在这里插入图片描述
所以我们可以使用物理世界的球体坐标来更新Three.js世界中的球体坐标,设置之后会看到上图中的球体从(0,3,0)的高处下落穿过地面,因为物理世界中还没有添加地面

	//更新Three.js世界球体的坐标
	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)

联系材质ContactMaterial

观察上面动图发现当球体下落与地面碰撞后没有非常明显的反弹行为。
我们可以通过设置材质Material来更改摩擦和反弹行为。

  1. 先创建混凝土和塑料材质
//创建混凝土材质
const concreteMaterial = new CANNON.Material('concrete')
//创建塑料材质
const plasticMaterial = new CANNON.Material('plastic')
  1. 创建联系材质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
  1. 或者我们也可以简化一下,用默认材质来替代上面的混凝土和塑料材质
//创建默认材质
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

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 = true
  mesh.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 - oldElapsedTime
  oldElapsedTime = elapsedTime
  //Update physics world
  world.step(1 / 60, deltaTime, 3)
  for(const object of objectToUpdate) {
    object.mesh.position.copy(object.body.position)
  // Update controls
  controls.update()
  // Render
  renderer.render(scene, camera)
  // Call tick again on the next frame
  window.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 = true
  mesh.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 = true
  mesh.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

当然我们也可以通过BodysleepSpeedLimit属性或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) {
    //移除刚体body
    object.body.removeEventListener('collide', playHitSound)
    world.removeBody(object.body)
    // 移除网格mesh
    scene.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 [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: 加载glsl的时候,vite,以字符串形式加载.glsl文件即可,即在文件后面加上“raw”参数即可: import basicVertextShader from './shader/basic/vertex.glsl?raw' import basicFragmentShader from './shader/basic/fragment.glsl?raw' three.js学习笔记(十三)——真实渲染 Yale Qi: THREE.GammaEncoding 属性还存在 three.js学习笔记(十三)——真实渲染 Yale Qi: //解决加载gltf格式模型纹理贴图和原图不一样问题 renderer.outputEncoding = THREE.sRGBEncoding; 上面这个新版本已经废弃默认就是SRGBColorSpace //新版本,加载gltf,不需要执行下面代码解决颜色偏差 renderer.outputColorSpace = THREE.SRGBColorSpace;//设置为SRGB颜色空间 文档 https://threejs.org/docs/index.html#api/zh/loaders/CubeTextureLoader three.js学习笔记(九)——光线投射 Yale Qi: Threejs的笔记太详细了,膜拜