ThreeJs 学习之旅(十六)—Physics(物理)

素材:

1.CANNON.js文档地址

schteppe/cannon.js @ GitHubhttps://schteppe.github.io/cannon.js/
cannonhttp://schteppe.github.io/cannon.js/docs/

2.CANNON-ES.js文档地址

cannon-esDocumentation for cannon-eshttps://pmndrs.github.io/cannon-es/docs/modules.html

初始代码 

import "./style.css";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import * as dat from "dat.gui";

import ky from "kyouka";
import { World } from "cannon-es";
import { Mesh } from "three";

/**
 * Debug
 */
const gui = new dat.GUI();

/**
 * Base
 */
// Canvas
const canvas = document.querySelector("canvas.webgl");
const textureLoader=new THREE.TextureLoader()

// Scene
const scene = new THREE.Scene();
/**
 * Light
 */
 const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
 scene.add(ambientLight);
 
 const directionalLight = new THREE.DirectionalLight(0xffffff, 0.2);
 directionalLight.castShadow = true;
 directionalLight.shadow.mapSize.set(1024, 1024);
 directionalLight.shadow.camera.far = 15;
 directionalLight.shadow.camera.left = -7;
 directionalLight.shadow.camera.top = 7;
 directionalLight.shadow.camera.right = 7;
 directionalLight.shadow.camera.bottom = -7;
 directionalLight.position.set(5, 5, 5);
 scene.add(directionalLight);
/**
 * Objects
 */
const sphere=new Mesh(
  new THREE.SphereBufferGeometry(0.5,20,20),
  new THREE.MeshStandardMaterial({
    metalness:0.3,
    roughness:0.4
  })
)
sphere.position.y=0.5
sphere.castShadow=true
scene.add(sphere) 
const floor=new Mesh(
  new THREE.PlaneBufferGeometry(10,10),
  new THREE.MeshStandardMaterial({
    color: "#777777",
    metalness: 0.3,
    roughness: 0.4,
  })
)
floor.castShadow=true
floor.receiveShadow = true;
floor.rotation.x = -Math.PI * 0.5;
scene.add(floor)
/**
 * Sizes
 */
const sizes = {
  width: window.innerWidth,
  height: window.innerHeight,
};

window.addEventListener("resize", () => {
  // Update sizes
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;

  // Update camera
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  // Update renderer
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(
  75,
  sizes.width / sizes.height,
  0.1,
  100
);
camera.position.set(-3, 3, 3);
scene.add(camera);

// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

renderer.shadowMap.enabled=true 
// sphereBody.applyLocalForce(new CANNON.Vec3(150, 0, 0));

const clock = new THREE.Clock();

const tick = () => {
  const elapsedTime = clock.getElapsedTime();

  // Update controls
  controls.update();

  // Render
  renderer.render(scene, camera);

  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};

tick();

 1.创建初始化CANNON环境

1) 创建世界

World | cannon-es

gravity gravity: Vec3

Defined in world/World.ts:80

The gravity of the world.

世界的引力。

const world=new CANNON.World()    //创建 cannon世界
world.gravity.set(0,-9.82,0)    //设置重力方向

2) 创建CANNON 球体

Body | cannon-es

const sphereBody=new CANNON.Body({
  mass:1,
  position:new CANNON.Vec3(0,3,0),
  shape:new CANNON.Sphere(0.5)
})

3) 将世界中加入物体

world.addBody(sphere)

4) 设置步进时间

 world.step(1/60,deltaTime,3)

wold.step

step step(dt: number, timeSinceLastCalled?: number, maxSubSteps?: number): void Defined in world/World.ts:470 Step the physics world forward in time. There are two modes. The simple mode is fixed timestepping without interpolation. In this case you only use the first argument. The second case uses interpolation. In that you also provide the time since the function was last used, as well as the maximum fixed timesteps to take.

步骤 step(dt: number,timeSinceLastCalled?:number,maxSubSteps?:编号):

无效 在world/World.ts:470中定义 让物理世界及时向前迈进。 有两种模式。简单模式是无插值的固定时间步长。在这种情况下,您只需使用第一个参数。第二种情况使用插值。因为您还提供了自上次使用该函数以来的时间,以及要使用的最大固定时间步长。

Parameters dt: number The fixed time step size to use. Optional timeSinceLastCalled: number The time elapsed since the function was last called. maxSubSteps: number = 10 Maximum number of fixed steps to take per function call (default: 10). Returns void

dt:        数字 要使用的固定时间步长。
timesinclastcalled:number 自上次调用该函数以来经过的时间。
maxSubSteps: number = 10 每次函数调用要执行的最大固定步骤数(默认值:10)。 返回void

5) 绑定 THREEjs CANNONjs 两个球体位置关联

sphere.position.copy(sphereBody.position)
const scene = new THREE.Scene();
const world=new CANNON.World()
world.gravity.set(0,-9.82,0)    //设置重力方向
const sphereBody=new CANNON.Body({
  mass:1,
  position:new CANNON.Vec3(0,3,0),
  shape:new CANNON.Sphere(0.5)
})
world.addBody(sphereBody)
...
...
...
const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  const deltaTime=elapsedTime-oldTime;

  world.step(1/60,deltaTime,3)
  sphere.position.copy(sphereBody.position)
  // Update controls
  controls.update();

  // Render
  renderer.render(scene, camera);

  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};


 6)创建平台

const floorBody=new CANNON.Body({
  mass:0,
  shape:new CANNON.Plane()
})
world.addBody(floorBody)

问题发现小球方向不对了,原因是 CANNON的平台方向不对

6) 反转平台

Quaternion | cannon-es

const floorBody=new CANNON.Body({
  mass:0,
  shape:new CANNON.Plane()
})
floorBody.quaternion.setFromAxisAngle(
  new CANNON.Vec3(-1,0,0),
  Math.PI*0.5
)
world.addBody(floorBody)

效果:

 7)给小球和平台添加材质

Material | cannon-es

const defaultMat=new CANNON.Material('default')
const defaultContactMaterial=new CANNON.ContactMaterial(defaultMat,defaultMat,{
  friction:0.6,
  restitution:0.3
})
world.addContactMaterial(defaultContactMaterial)

给小球和平台也加上材质

const sphereBody=new CANNON.Body({
  mass:1,
  position:new CANNON.Vec3(0,3,0),
  shape:new CANNON.Sphere(0.5),
  material:defaultContactMaterial
})
world.addBody(sphereBody)

const floorBody=new CANNON.Body({
  mass:0,
  shape:new CANNON.Plane(),
  material:defaultContactMaterial
})
floorBody.quaternion.setFromAxisAngle(
  new CANNON.Vec3(-1,0,0),
  Math.PI*0.5
  
)
world.addBody(floorBody)
/**

效果:

 8)给一个小球一个方向力

Body | cannon-es

const sphereBody=new CANNON.Body({
  mass:1,
  position:new CANNON.Vec3(0,3,0),
  shape:new CANNON.Sphere(0.5),
  material:defaultContactMaterial
})
sphereBody.applyLocalForce(new Vec3(150,0,0))
world.addBody(sphereBody)

 2.优化创建代码

let objsToUpdate=[]
const sphere=new THREE.SphereBufferGeometry(1,20,20)
 const sphereMateral=new THREE.MeshStandardMaterial({
   roughness:0.4,
   metalness:0.3,
 })
const createSphere=(r,ps)=>{
  const mesh=new Mesh(sphere,sphereMateral)

  mesh.castShadow=true
  mesh.position.copy(ps)
  scene.add(mesh)

  const body=new CANNON.Body({
    mass:1,
    position:new Vec3().copy(ps),
    shape:new Sphere(r),
    material:defaultContactMaterial
  })
  objsToUpdate.push({mesh,body})
  world.addBody(body)
}
createSphere(0.5,{x:0,y:3,z:0})


...
...
...

const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  const deltaTime=elapsedTime-oldTime;

  objsToUpdate.forEach(obj=>{
    obj.mesh.quaternion.copy(obj.body.quaternion)
    obj.mesh.position.copy(obj.body.position)
  })
  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);
};

效果:

 问题 太大了且穿模


 2.1) 设置缩放

  mesh.scale.set(r,r,r)



//完整部分
const createSphere=(r,ps)=>{
  const mesh=new Mesh(sphere,sphereMateral)
  mesh.scale.set(r,r,r)
  mesh.castShadow=true
  mesh.position.copy(ps)
  scene.add(mesh)

  const body=new CANNON.Body({
    mass:1,
    position:new Vec3().copy(ps),
    shape:new Sphere(r),
    material:defaultContactMaterial
  })
  objsToUpdate.push({mesh,body})
  world.addBody(body)
}

 效果:

 3.使用gui 调式创建多个球体

const guiObj={
  
}
guiObj.createSphere=()=>{
  createSphere(Math.random()*0.5,{x:(Math.random()-0.5)*3,y:3,z:(Math.random()-0.5)*3})
}
gui.add(guiObj,'createSphere')

效果:

3.创建方块

注意 CANNON Box 需要的 是

const box=new THREE.BoxBufferGeometry(1,1,1)
const boxMaterial=new THREE.MeshStandardMaterial({
  roughness:0.4,
  metalness:0.3,
})
const createBox=(width,height,depth,position)=>{
  const mesh=new Mesh(box,boxMaterial)
   mesh.scale.set(width,height,depth)
  mesh.castShadow=true
  mesh.position.copy(position)
  const body=new CANNON.Body({
    mass:1,
    position:new Vec3().copy(position),
    shape:new CANNON.Box(new CANNON.Vec3(width,height,depth)),
    material:defaultContactMaterial
  })
  objsToUpdate.push({mesh,body})
  scene.add(mesh)
  world.addBody(body)
}

...
...
...
guiObj.createBox=()=>{
   createBox(Math.random()*0.5,Math.random()*0.5,Math.random()*0.5,{x:(Math.random()-0.5)*3,y:3,z:(Math.random()-0.5)*3})
}
gui.add(guiObj,'createBox')

效果:

4.碰撞检测性能优化


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 —— 如果刚体在这几秒钟内一直处于沉睡,则视为处于睡眠状态。

5.添加碰撞音效 

通过collide 来监听 碰撞事件

const audio=new Audio("sounds/hit.mp3")
const playHitSound=()=>{
  audio.play()
}

const createBox=(width,height,depth,position)=>{
  const mesh=new Mesh(box,boxMaterial)
   mesh.scale.set(width,height,depth)
  mesh.castShadow=true
  mesh.position.copy(position)
  const body=new CANNON.Body({
    mass:1,
    position:new Vec3().copy(position),
    shape:new CANNON.Box(new CANNON.Vec3(width,height,depth)),
    material:defaultContactMaterial
  })
  body.addEventListener("collide",playHitSound)

  objsToUpdate.push({mesh,body})
  scene.add(mesh)
  world.addBody(body)
}

问题 这样创建的声音很违和都是一样的,

需求力度大的时候有声音

优化音效

碰撞强度可以通过contact属性中的getImpactVelocityAlongNormal()方法获取到

因此我们只要当碰撞强度大于某个值时再触发音效就行了

const playHitSound=(collision)=>{
  if(collision.contact.getImpactVelocityAlongNormal()>1.5){
    hitSound.currentTime=0;
    hitSound.volume=Math.random()
    hitSound.play();
  }
}

Rec 0003

5.重置画布

guiObj.restCanvas=()=>{
  objsToUpdate.forEach(obj=>{
    scene.remove(obj.mesh)
    world.removeBody(obj.body)
    obj.body.removeEventListener("collide",()=>playHitSound())

  })
}

gui.add(guiObj,'restCanvas')

效果:

6.Web Worker

由于JavaScript是单线程模型,即所有任务只能在同一个线程上面完成,前面的任务没有做完,后面的就只能等待,这对于日益增强的计算能力来说不是一件好事。所以在HTML5中引入了Web Worker的概念,来为JavaScript创建多线程环境,将其中一些任务分配给Web Worker运行,二者可以同时运行,互不干扰。Web Worker是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。

在计算机中做物理运算的是CPU,负责WebGL图形渲染的是GPU。现在我们的所有事情都是在CPU中的同一个线程完成的,所以该线程可能很快就过载,而解决方案就是使用worker。
我们通常把进行物理计算的部分放到worker里面,具体可看这个例子的源码

cannon.js worker example

7.本期完整代码:

import "./style.css";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import * as dat from "dat.gui";
import * as CANNON from 'cannon-es'
import ky from "kyouka";
import { Mesh } from "three";
import { Sphere, Vec3 } from "cannon-es";

/**
 * Debug
 */
const gui = new dat.GUI();
const hitSound=new Audio("sounds/hit.mp3")
const playHitSound=(collision)=>{
  if(collision.contact.getImpactVelocityAlongNormal()>1.5){
    hitSound.currentTime=0;
    hitSound.volume=Math.random()
    hitSound.play();
  }
}
/**
 * Base
 */
// Canvas
const canvas = document.querySelector("canvas.webgl");
const textureLoader=new THREE.TextureLoader()
let objsToUpdate=[]
// Scene
const scene = new THREE.Scene();
const world=new CANNON.World()
world.gravity.set(0,-9.82,0)    //设置重力方向
const defaultMat=new CANNON.Material('default')
const defaultContactMaterial=new CANNON.ContactMaterial(defaultMat,defaultMat,{
  friction:0.1,
  restitution:0.6
})
world.addContactMaterial(defaultContactMaterial)

world.broadphase=new CANNON.SAPBroadphase(world)
world.allowSleep=true
const floorBody=new CANNON.Body({
  mass:0,
  shape:new CANNON.Plane(),
  material:defaultContactMaterial
})
floorBody.quaternion.setFromAxisAngle(
  new CANNON.Vec3(-1,0,0),
  Math.PI*0.5
  
)
world.addBody(floorBody)
/**
 * Light
 */
 const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
 scene.add(ambientLight);
 
 const directionalLight = new THREE.DirectionalLight(0xffffff, 0.2);
 directionalLight.castShadow = true;
 directionalLight.shadow.mapSize.set(1024, 1024);
 directionalLight.shadow.camera.far = 15;
 directionalLight.shadow.camera.left = -7;
 directionalLight.shadow.camera.top = 7;
 directionalLight.shadow.camera.right = 7;
 directionalLight.shadow.camera.bottom = -7;
 directionalLight.position.set(5, 5, 5);
 scene.add(directionalLight);
/**
 * Objects
 */
 const sphere=new THREE.SphereBufferGeometry(1,20,20)
 const sphereMateral=new THREE.MeshStandardMaterial({
   roughness:0.4,
   metalness:0.3,
 })
 const box=new THREE.BoxBufferGeometry(1,1,1)
 const boxMaterial=new THREE.MeshStandardMaterial({
  roughness:0.4,
  metalness:0.3,
})
const createBox=(width,height,depth,position)=>{
  const mesh=new Mesh(box,boxMaterial)
   mesh.scale.set(width,height,depth)
  mesh.castShadow=true
  mesh.position.copy(position)
  const body=new CANNON.Body({
    mass:1,
    position:new Vec3().copy(position),
    shape:new CANNON.Box(new CANNON.Vec3(width,height,depth)),
    material:defaultContactMaterial
  })
  body.addEventListener("collide",playHitSound)

  objsToUpdate.push({mesh,body})
  scene.add(mesh)
  world.addBody(body)
}
const createSphere=(r,ps)=>{
  const mesh=new Mesh(sphere,sphereMateral)
  mesh.scale.set(r,r,r)
  mesh.castShadow=true
  mesh.position.copy(ps)
  scene.add(mesh)

  const body=new CANNON.Body({
    mass:1,
    position:new Vec3().copy(ps),
    shape:new Sphere(r),
    material:defaultContactMaterial
  })
  objsToUpdate.push({mesh,body})
  world.addBody(body)
}
createSphere(0.5,{x:0,y:3,z:0})
const floor=new Mesh(
  new THREE.PlaneBufferGeometry(10,10),
  new THREE.MeshStandardMaterial({
    color: "#777777",
    metalness: 0.3,
    roughness: 0.4,
  })
)
floor.castShadow=true
floor.receiveShadow = true;
floor.rotation.x = -Math.PI * 0.5;
scene.add(floor)
/**
 * Sizes
 */
const sizes = {
  width: window.innerWidth,
  height: window.innerHeight,
};

window.addEventListener("resize", () => {
  // Update sizes
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;

  // Update camera
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  // Update renderer
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(
  75,
  sizes.width / sizes.height,
  0.1,
  100
);
camera.position.set(-3, 3, 3);
scene.add(camera);

// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

renderer.shadowMap.enabled=true 
// sphereBody.applyLocalForce(new CANNON.Vec3(150, 0, 0));

const clock = new THREE.Clock();
const oldTime=0;
const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  const deltaTime=elapsedTime-oldTime;

  objsToUpdate.forEach(obj=>{
    obj.mesh.quaternion.copy(obj.body.quaternion)
    obj.mesh.position.copy(obj.body.position)
  })
  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);
};

tick();

const guiObj={
  
}
guiObj.createSphere=()=>{
  createSphere(Math.random()*0.5,{x:(Math.random()-0.5)*3,y:3,z:(Math.random()-0.5)*3})
}
guiObj.createBox=()=>{
   createBox(Math.random()*0.5,Math.random()*0.5,Math.random()*0.5,{x:(Math.random()-0.5)*3,y:3,z:(Math.random()-0.5)*3})
}
guiObj.restCanvas=()=>{
  objsToUpdate.forEach(obj=>{
    scene.remove(obj.mesh)
    world.removeBody(obj.body)
    obj.body.removeEventListener("collide",()=>playHitSound())

  })
}
gui.add(guiObj,'createSphere')
gui.add(guiObj,'createBox')
gui.add(guiObj,'restCanvas')

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值