素材:
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) 创建世界
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 球体
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) 反转平台
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)给小球和平台添加材质
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)给一个小球一个方向力
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里面,具体可看这个例子的源码
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')