threejs(9)-应用物理引擎设置物体相互作用

一、认识物理引擎与cannon安装

Cannon.js是一个开源的3D物理引擎,用于在WebGL中创建3D物理模拟。它提供了一个灵活的API,可以应用于许多WebGL场景。

我们需要了解一些基本概念,包括物理实体、碰撞、物理变换、物理世界和物理实体之间的约束等。

threejs(9)-应用物理引擎设置物体相互作用_第1张图片

官网:https://pmndrs.github.io/cannon-es/

npm: https://www.npmjs.com/package/cannon-es

npm i --save cannon-es

cannon-es是一个轻量级且易于使用的网络 3D 物理引擎。它的灵感来自three.js的简单 API,并基于ammo.js和Bullet 物理引擎。
首先要设置的是我们的物理世界,它将容纳我们所有的物理实体并推动模拟向前发展。
让我们用地球引力创造一个世界。请注意,cannon.js 使用SI 单位(米、千克、秒等)。

const world = new CANNON.World({
  gravity: new CANNON.Vec3(0, -9.82, 0), // m/s²
})

为了推进模拟,我们必须调用world.fixedStep()每一帧。作为第一个参数,我们可以传递我们希望模拟运行的固定时间步长,默认值是1 / 60meaning 60fps。 world.fixedStep()跟踪上次调用它以独立于帧率保持相同速度的模拟,因为requestAnimationFrame调用可能因不同设备而异,或者如果存在性能问题。在此处阅读有关固定模拟步进的更多信息。

function animate() {
  requestAnimationFrame(animate)

  // Run the simulation independently of framerate every 1 / 60 ms
  world.fixedStep()
}
// Start the simulation loop
animate()

如果你想打发自上次通话以来的时间(dt在游戏世界中),你可以使用更高级的world.step().
查看高级世界步进示例

const timeStep = 1 / 60 // seconds
let lastCallTime
function animate() {
  requestAnimationFrame(animate)

  const time = performance.now() / 1000 // seconds
  if (!lastCallTime) {
    world.step(timeStep)
  } else {
    const dt = time - lastCallTime
    world.step(timeStep, dt)
  }
  lastCallTime = time
}
// Start the simulation loop
animate()

刚体是将在世界中模拟的实体,它们可以是简单的形状,如Sphere、Box、Plane、Cylinder,或更复杂的形状,如ConvexPolyhedron、Particle、Heightfield、Trimesh。
让我们创建一个基本的球体。

const radius = 1 // m
const sphereBody = new CANNON.Body({
  mass: 5, // kg
  shape: new CANNON.Sphere(radius),
})
sphereBody.position.set(0, 10, 0) // m
world.addBody(sphereBody)

如您所见,我们指定了一个质量属性,质量定义了身体在受力影响时的行为。
当物体具有质量并受到力的影响时,它们被称为动态物体。还有不受力影响但可以具有速度并四处移动的运动学实体。第三种类型的物体是静态物体,它们只能在世界中定位并且不受力或速度的影响。
如果将质量为 0 的物体传递给物体,则该物体会自动标记为静态物体。您还可以在主体选项中明确主体类型。例如,让我们创建一个静态地面。

const groundBody = new CANNON.Body({
  type: CANNON.Body.STATIC,
  shape: new CANNON.Plane(),
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // make it face up
world.addBody(groundBody)

以下是所有以前的片段组合在一个完整的示例中。

import * as CANNON from 'cannon-es'

// Setup our physics world
const world = new CANNON.World({
  gravity: new CANNON.Vec3(0, -9.82, 0), // m/s²
})

// Create a sphere body
const radius = 1 // m
const sphereBody = new CANNON.Body({
  mass: 5, // kg
  shape: new CANNON.Sphere(radius),
})
sphereBody.position.set(0, 10, 0) // m
world.addBody(sphereBody)

// Create a static plane for the ground
const groundBody = new CANNON.Body({
  type: CANNON.Body.STATIC, // can also be achieved by setting the mass to 0
  shape: new CANNON.Plane(),
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // make it face up
world.addBody(groundBody)

// Start the simulation loop
function animate() {
  requestAnimationFrame(animate)

  world.fixedStep()

  // the sphere y position shows the sphere falling
  console.log(`Sphere y position: ${sphereBody.position.y}`)
}
animate()

请注意,cannon 不负责将任何内容渲染到屏幕上,它只是计算模拟的数学。要在屏幕上实际显示某些内容,您必须使用渲染库,例如three.js。让我们看看如何实现这一目标。
首先,你必须在 three.js 中创建 body 的对应实体。例如,这里是如何在 three.js 中创建一个球体。

const radius = 1 // m
const geometry = new THREE.SphereGeometry(radius)
const material = new THREE.MeshNormalMaterial()
const sphereMesh = new THREE.Mesh(geometry, material)
scene.add(sphereMesh)

然后,您必须将 three.js 网格与 cannon.js 主体连接起来。要做到这一点,您需要在走过世界后每帧将位置和旋转数据从身体复制到网格。

function animate() {
  requestAnimationFrame(animate)

  // world stepping...

  sphereMesh.position.copy(sphereBody.position)
  sphereMesh.quaternion.copy(sphereBody.quaternion)

  // three.js render...
}
animate()

二、使用物理引擎关联Threejs物体

threejs(9)-应用物理引擎设置物体相互作用_第2张图片
一个物理引擎,一边渲染引擎,渲染引擎从物理引擎中获取数据进行渲染
threejs(9)-应用物理引擎设置物体相互作用_第3张图片

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

// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);

//设置物体材质
const sphereWorldMaterial = new CANNON.Material();

// 创建物理世界的物体
const sphereBody = new CANNON.Body({
  shape: sphereShape,
  position: new CANNON.Vec3(0, 0, 0),
  //   小球质量
  mass: 1,
  //   物体材质
  material: sphereWorldMaterial,
});

// 将物体添加至物理世界
world.addBody(sphereBody);

物理引擎如何与渲染引擎关联

 // 更新物理引擎里世界的物体
  world.step(1 / 120, deltaTime); // 更新
  sphere.position.copy(sphereBody.position); // 渲染引擎复制物理引擎中的数据 做渲染 自由落体

自由落体

import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";

// 目标:使用cannon引擎
console.log(CANNON);

// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  300
);

// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);

// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true; // 阴影
scene.add(sphere);

const floor = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(20, 20),
  new THREE.MeshStandardMaterial()
);

floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;// 接收阴影
scene.add(floor);

// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);

//设置物体材质
const sphereWorldMaterial = new CANNON.Material();

// 创建物理世界的物体
const sphereBody = new CANNON.Body({
  shape: sphereShape,
  position: new CANNON.Vec3(0, 0, 0),
  //   小球质量
  mass: 1,
  //   物体材质
  material: sphereWorldMaterial,
});

// 将物体添加至物理世界
world.addBody(sphereBody);

//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true; // 阴影
scene.add(dirLight);

// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;

// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);

// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();

function render() {
  //   let time = clock.getElapsedTime();
  let deltaTime = clock.getDelta();
  // 更新物理引擎里世界的物体
  world.step(1 / 120, deltaTime); // 更新

  sphere.position.copy(sphereBody.position); // 渲染引擎复制物理引擎中的数据

  renderer.render(scene, camera);
  //   渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
  //   console.log("画面变化了");

  // 更新摄像头
  camera.aspect = window.innerWidth / window.innerHeight;
  //   更新摄像机的投影矩阵
  camera.updateProjectionMatrix();

  //   更新渲染器
  renderer.setSize(window.innerWidth, window.innerHeight);
  //   设置渲染器的像素比
  renderer.setPixelRatio(window.devicePixelRatio);
});

三、设置固定不动的地面与小球碰撞

小球如何撞到地面之后停下来

那么也要创建一个物理世界地面

// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);

这样小球打在地面上就会停下来
threejs(9)-应用物理引擎设置物体相互作用_第4张图片

import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";

// 目标:使用cannon引擎
console.log(CANNON);

// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  300
);

// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);

// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true;
scene.add(sphere);

const floor = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(20, 20),
  new THREE.MeshStandardMaterial()
);

floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);

// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);

//设置物体材质
const sphereWorldMaterial = new CANNON.Material();

// 创建物理世界的物体
const sphereBody = new CANNON.Body({
  shape: sphereShape,
  position: new CANNON.Vec3(0, 0, 0),
  //   小球质量
  mass: 1,
  //   物体材质
  material: sphereWorldMaterial,
});

// 将物体添加至物理世界
world.addBody(sphereBody);

// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);

//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true;
scene.add(dirLight);

// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;

// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);

// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();

function render() {
  //   let time = clock.getElapsedTime();
  let deltaTime = clock.getDelta();
  // 更新物理引擎里世界的物体
  world.step(1 / 120, deltaTime);

  sphere.position.copy(sphereBody.position);

  renderer.render(scene, camera);
  //   渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
  //   console.log("画面变化了");

  // 更新摄像头
  camera.aspect = window.innerWidth / window.innerHeight;
  //   更新摄像机的投影矩阵
  camera.updateProjectionMatrix();

  //   更新渲染器
  renderer.setSize(window.innerWidth, window.innerHeight);
  //   设置渲染器的像素比
  renderer.setPixelRatio(window.devicePixelRatio);
});

三、监听碰撞事件和控制碰撞音效

threejs(9)-应用物理引擎设置物体相互作用_第5张图片

// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");
// 添加监听碰撞事件
function HitEvent(e) {
  // 获取碰撞的强度
  //   console.log("hit", e);
  const impactStrength = e.contact.getImpactVelocityAlongNormal();
  console.log(impactStrength); // 获取碰撞的强度
  if (impactStrength > 2) {
    //   重新从零开始播放
    hitSound.currentTime = 0;
    hitSound.play();
  }
}
sphereBody.addEventListener("collide", HitEvent);

免费音乐素材下载地址,爱给网:https://www.aigei.com/

import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";

// 目标:使用cannon引擎
console.log(CANNON);

// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  300
);

// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);

// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true;
scene.add(sphere);

const floor = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(20, 20),
  new THREE.MeshStandardMaterial()
);

floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);

// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);

//设置物体材质
const sphereWorldMaterial = new CANNON.Material("sphere");

// 创建物理世界的物体
const sphereBody = new CANNON.Body({
  shape: sphereShape,
  position: new CANNON.Vec3(0, 0, 0),
  //   小球质量
  mass: 1,
  //   物体材质
  material: sphereWorldMaterial,
});

// 将物体添加至物理世界
world.addBody(sphereBody);

// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");
// 添加监听碰撞事件
function HitEvent(e) {
  // 获取碰撞的强度
  //   console.log("hit", e);
  const impactStrength = e.contact.getImpactVelocityAlongNormal();
  console.log(impactStrength); // 获取碰撞的强度
  if (impactStrength > 2) {
    //   重新从零开始播放
    hitSound.currentTime = 0;
    hitSound.play();
  }
}
sphereBody.addEventListener("collide", HitEvent);

// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);

// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
  sphereMaterial,
  floorMaterial,
  {
    //   摩擦力
    friction: 0.1,
    // 弹性
    restitution: 0.7,
  }
);

// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);

//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true;
scene.add(dirLight);

// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;

// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);

// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();

function render() {
  //   let time = clock.getElapsedTime();
  let deltaTime = clock.getDelta();
  // 更新物理引擎里世界的物体
  world.step(1 / 120, deltaTime);

  sphere.position.copy(sphereBody.position);

  renderer.render(scene, camera);
  //   渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
  //   console.log("画面变化了");

  // 更新摄像头
  camera.aspect = window.innerWidth / window.innerHeight;
  //   更新摄像机的投影矩阵
  camera.updateProjectionMatrix();

  //   更新渲染器
  renderer.setSize(window.innerWidth, window.innerHeight);
  //   设置渲染器的像素比
  renderer.setPixelRatio(window.devicePixelRatio);
});

四、关联材质设置摩擦与弹性系数

//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");
// 设置地面材质
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;

// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
  cubeWorldMaterial,
  floorMaterial,
  {
    //   摩擦力
    friction: 0.1,
    // 弹性
    restitution: 0.7,
  }
);

// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);

// 设置世界碰撞的默认材料,如果材料没有设置,都用这个
world.defaultContactMaterial = defaultContactMaterial;

五、立方体相互碰撞后旋转效果

// window每点击一次创建一个物体
threejs(9)-应用物理引擎设置物体相互作用_第6张图片

window.addEventListener("click", createCube);

const cubeArr = [];
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");

function createCube() {
  // 创建立方体和平面
  const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
  const cubeMaterial = new THREE.MeshStandardMaterial();
  const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
  cube.castShadow = true; //阴影
  scene.add(cube);
  // 创建物理cube形状
  const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));

  // 创建物理世界的物体
  const cubeBody = new CANNON.Body({
    shape: cubeShape,
    position: new CANNON.Vec3(0, 0, 0),
    //   小球质量
    mass: 1,
    //   物体材质
    material: cubeWorldMaterial,
  });
  cubeBody.applyLocalForce(
    new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
    new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
  );

  // 将物体添加至物理世界
  world.addBody(cubeBody);
  // 添加监听碰撞事件
  function HitEvent(e) {
    // 获取碰撞的强度
    //   console.log("hit", e);
    const impactStrength = e.contact.getImpactVelocityAlongNormal();
    console.log(impactStrength);
    if (impactStrength > 2) {
      //   重新从零开始播放
      hitSound.currentTime = 0;
      hitSound.volume = impactStrength / 12;
      hitSound.play();
    }
  }
  cubeBody.addEventListener("collide", HitEvent);
  cubeArr.push({
    mesh: cube,
    body: cubeBody,
  });
}

// 下落后物体旋转

  cubeArr.forEach((item) => {
    item.mesh.position.copy(item.body.position);
    // 设置渲染的物体跟随物理的物体旋转
    item.mesh.quaternion.copy(item.body.quaternion);
  });

撞击后声音逐渐减弱

  // 添加监听碰撞事件
  function HitEvent(e) {
    // 获取碰撞的强度
    //   console.log("hit", e);
    const impactStrength = e.contact.getImpactVelocityAlongNormal();
    console.log(impactStrength);
    if (impactStrength > 2) {
      //   重新从零开始播放
      hitSound.currentTime = 0;
      hitSound.volume = impactStrength / 12;
      hitSound.play();
    }
  }
  cubeBody.addEventListener("collide", HitEvent);

六、给物体施加作用力

  cubeBody.applyLocalForce(
    new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
    new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
  );
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";

// 目标:设置cube跟着旋转
console.log(CANNON);

// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  300
);

// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);

const cubeArr = [];
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");

function createCube() {
  // 创建立方体和平面
  const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
  const cubeMaterial = new THREE.MeshStandardMaterial();
  const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
  cube.castShadow = true; //阴影
  scene.add(cube);
  // 创建物理cube形状
  const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));

  // 创建物理世界的物体
  const cubeBody = new CANNON.Body({
    shape: cubeShape,
    position: new CANNON.Vec3(0, 0, 0),
    //   小球质量
    mass: 1,
    //   物体材质
    material: cubeWorldMaterial,
  });
  cubeBody.applyLocalForce(
    new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
    new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
  );

  // 将物体添加至物理世界
  world.addBody(cubeBody);
  // 添加监听碰撞事件
  function HitEvent(e) {
    // 获取碰撞的强度
    //   console.log("hit", e);
    const impactStrength = e.contact.getImpactVelocityAlongNormal();
    console.log(impactStrength);
    if (impactStrength > 2) {
      //   重新从零开始播放
      hitSound.currentTime = 0;
      hitSound.volume = impactStrength / 12;
      hitSound.play();
    }
  }
  cubeBody.addEventListener("collide", HitEvent);
  cubeArr.push({
    mesh: cube,
    body: cubeBody,
  });
}

window.addEventListener("click", createCube);

// 平面
const floor = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(20, 20),
  new THREE.MeshStandardMaterial()
);

floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true; // 接收阴影
scene.add(floor);

// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0); // 重力方向

// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");

// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
// 设置地面材质
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);

// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
  cubeWorldMaterial,
  floorMaterial,
  {
    //   摩擦力
    friction: 0.1,
    // 弹性
    restitution: 0.7,
  }
);

// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);

// 设置世界碰撞的默认材料,如果材料没有设置,都用这个
world.defaultContactMaterial = defaultContactMaterial;

//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true; // 投放阴影
scene.add(dirLight);

// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;

// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);

// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();

function render() {
  //   let time = clock.getElapsedTime();
  let deltaTime = clock.getDelta();
  // 更新物理引擎里世界的物体
  world.step(1 / 120, deltaTime); // 更新

  //   cube.position.copy(cubeBody.position);
  cubeArr.forEach((item) => {
    item.mesh.position.copy(item.body.position);
    // 设置渲染的物体跟随物理的物体旋转
    item.mesh.quaternion.copy(item.body.quaternion);
  });

  renderer.render(scene, camera);
  //   渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
  //   console.log("画面变化了");

  // 更新摄像头
  camera.aspect = window.innerWidth / window.innerHeight;
  //   更新摄像机的投影矩阵
  camera.updateProjectionMatrix();

  //   更新渲染器
  renderer.setSize(window.innerWidth, window.innerHeight);
  //   设置渲染器的像素比
  renderer.setPixelRatio(window.devicePixelRatio);
});

七、cannon流体模拟

流体模拟是一种用于模拟流体运动和形态的技术,常用于计算机图形学、动画、物理学、流体力学等领域。
在计算机图形学中,流体模拟可以用来创建各种真实的水、火、烟、云等效果。如海浪、河流、火焰、烟雾、雨雪等。
在动画制作中,流体模拟可以用来制作角色的衣服、头发、发型等效果,还可以用来制作特效动画,如爆炸、火焰等。
在物理学和流体力学中,流体模拟可以用来研究流体运动的规律,如水流、气流、火焰等,对于工程设计和模拟有很大的帮助。
在游戏开发中,流体模拟可以用来制作游戏中的水、火、烟等效果,让游戏画面更加真实。
流体模拟技术还可以应用于医学影像、纺织工业、液压机械、化学工程等其他领域。
SPH (Smooth Particle Hydrodynamics) 是一种流体模拟算法,它可以模拟流体的运动和形态。SPH算法的核心思想是将流体分成许多粒子,然后用这些粒子来模拟流体的运动。
SPH算法的主要步骤如下:

  1. 初始化:在初始状态下,将流体划分成许多粒子,并计算每个粒子的密度、速度和位置。
  2. 计算力学量:在每一时刻,根据流体的物理性质和流体周围粒子的密度、速度和位置计算每个粒子的力学量,如加速度、速度和位置。
  3. 更新粒子状态:根据力学量更新粒子的速度和位置。
  4. 渲染:将粒子的位置和密度渲染成流体的形态。

SPH算法还有很多优化和变种,如模拟不同流体(如气体和液体)、多相流体、多孔介质、粘性流体、非牛顿流体等等。

需要注意的是,在实际应用中,SPH算法需要解决的问题非常复杂,需要对数学和物理有很好的理解。如果不熟悉这些知识,可能需要学习一些基础知识。
Cannon.js是一个开源的物理引擎,它支持WebGL和JavaScript。Cannon.js中的SPHSystem类是用来模拟流体的一个类,它使用了SPH算法。
使用Cannon.js中的SPHSystem类,可以很容易地在WebGL中模拟流体。下面是一个简单的例子,说明了如何使用SPHSystem类:

// 创建一个SPH系统
var sph = new CANNON.SPHSystem();

// 添加一些粒子
for (var i = 0; i < 100; i++) {
    var p = new CANNON.SPHSystem.Particle();
    p.position.set(Math.random(), Math.random(), Math.random());
    sph.addParticle(p);
}

// 每一帧更新粒子状态
function update() {
    sph.step();
    requestAnimationFrame(update);
}
update();

这样就可以在WebGL中模拟流体。需要注意的是,还需要使用WebGL来渲染粒子,并且需要设置相应的参数才能达到期望的效果。
CANNON.SPHSystem 还支持设置流体的密度、粘性、阻力等参数,还支持给粒子施加力,添加障碍物等等。需要根据实际需求来设置相应的参数。
CANNON.SPHSystem类中有一些属性来控制流体的表现,这四个属性分别是:
● density : 流体的密度,它用来控制流体的粘度和阻力。
● particles : 包含所有粒子的数组。
● smoothingRadius : 用来控制粒子之间交互的距离,粒子之间的距离小于这个值时,它们之间会产生影响。
● viscosity : 流体的粘度,它用来控制流体的阻力。
其中 density 和 viscosity 是控制流体行为的重要参数。密度越高,流体就会越粘稠,阻力也会越大。粘度越高,流体就会越阻力,移动越困难。
particles 属性是粒子数组,可以通过它访问到粒子的属性,如位置、速度等。
smoothingRadius 是交互半径,这个值越大,粒子之间的交互就越广,越小,粒子之间的交互就越窄,根据实际需求来调整。
如果想要模拟出真实的流体效果,需要根据实际场景来调整这些参数。

八、车辆模拟

RaycastVehicle
RaycastVehicle是cannon.js提供了一个车辆对象。并且官方给我们提供了demo。

何为Raycast
为何称为RaycastVehicle呢?这与该对象的物理模拟原理有关系。该对象使用刚体(CANNON.Body)作为车身,从刚体的四个角处向下发射固定长度的射线,射线与地面的交叉点作为车辆与地面的接触点,在该点为车身刚体施加纵向的悬挂弹力与横向的牵引摩擦力。

threejs(9)-应用物理引擎设置物体相互作用_第7张图片
其核心计算在updateVehicle方法中。

使用方法

使用方法可以参考官方示例。
1、首先构造一个RaycastVehicle对象:

vehicle = new CANNON.RaycastVehicle({
  chassisBody: chassisBody,
  indexRightAxis: 0,
  indexForwardAxis: 2,
  indexUpAxis: 1,
});
vehicle.addToWorld(world);

其中,chassisBody是代表车身的刚体,indexRightAxis、indexForwardAxis、indexUpAxis官方示例中并没有用到,他们分别代表车的右、前、上轴,0、1、2分别代表x、y、z轴。
2、添加逻辑车轮:

vehicle.addWheel(options);

options对象为车轮及悬挂的参数:

chassisConnectionPointLocal: new Vec3(),// 车轮连接点,相对于chassisBody(也是发射射线的起点)
  directionLocal: new Vec3(),// 车轮的下方方向(垂直车身向下)
  axleLocal: new Vec3(),// 车轴方向
  suspensionRestLength: 1,// 悬挂长度(未受任何力)
  suspensionMaxLength: 2,// 悬挂最大长度,限制计算出的suspensionLength
  suspensionStiffness: 100,// 悬挂刚度
  dampingCompression: 10,// 悬挂压缩阻尼
  dampingRelaxation: 10,// 悬挂复原阻尼
  maxSuspensionForce: Number.MAX_VALUE, // 限制计算出的suspensionForce
  maxSuspensionTravel: 1,// 悬挂可伸长或压缩的最大距离
  radius: 1,// 车轮半径
  frictionSlip: 10000,// 滑动摩檫系数(用于计算车轮所能提供的最大摩檫力)
  rollInfluence: 0.01,// 施加侧向力时的位置系数,越小越接近车身,防止侧翻

添加车轮碰撞。添加好逻辑车轮后,还要在world中添加刚体以产生碰撞:
在cannon-es的RaycastVehicle类中,设置轮子的对象包含以下属性:

  1. radius:轮子的半径,表示轮子的大小。
  2. directionLocal:轮子方向的本地坐标,指定轮子相对于车身的朝向。
  3. suspensionStiffness:悬挂硬度,指定悬挂系统的硬度,影响轮子的弹性。
  4. suspensionRestLength:悬挂系统的静态长度,指定悬挂系统在不受载荷时的长度。
  5. frictionSlip:滑动摩擦,指定轮子与地面的滑动摩擦。
  6. dampingRelaxation:弛性阻尼,指定悬挂系统的弛性阻尼,影响轮子的悬挂效果。
  7. dampingCompression:压缩阻尼,指定悬挂系统的压缩阻尼,影响轮子的悬挂效果。
  8. maxSuspensionForce:最大悬挂力,指定悬挂系统所能承受的最大悬挂力。
  9. rollInfluence:滚动影响,指定轮子的滚动影响,影响车辆的滚动效果。
  10. axleLocal:轴的本地坐标,指定轴相对于车身的位置。
  11. chassisConnectionPointLocal:车身连接点的本地坐标,指定轮子与车身的连接点的位置。
  12. maxSuspensionTravel:最大悬挂旅行,指定悬挂系统可以移动的最大距离,也就是轮子可以上下移动的最大距离。这个属性用来限制轮子的移动范围,有助于模拟轮子的物理行为。
for (var i = 0; i < vehicle.wheelInfos.length; i++) {
  var wheel = vehicle.wheelInfos[i];
  var cylinderShape = new CANNON.Cylinder(wheel.radius, wheel.radius, wheel.radius / 2, 20);
  var wheelBody = new CANNON.Body({
    mass: 0
  });
  wheelBody.type = CANNON.Body.KINEMATIC;
  wheelBody.collisionFilterGroup = 0; // turn off collisions
  var q = new CANNON.Quaternion();
  q.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), Math.PI / 2);// 把竖着的圆柱体放倒作为车轮
  wheelBody.addShape(cylinderShape, new CANNON.Vec3(), q);
  wheelBodies.push(wheelBody);
  world.addBody(wheelBody);

对于车轮碰撞,还要实时更新其Transom:

world.addEventListener('postStep', function () {
    for (var i = 0; i < vehicle.wheelInfos.length; i++) {
        var t = vehicle.wheelInfos[i].worldTransform;
        var wheelBody = wheelBodies[i];
        wheelBody.position.copy(t.position);
        wheelBody.quaternion.copy(t.quaternion);
        }
});

车辆控制。可以使用RaycastVehicle的如下函数控制车辆:

applyEngineForce = function(value, wheelIndex) // 施加牵引力
setSteeringValue = function(value, wheelIndex) // 设置转向角(弧度)
setBrake = function(brake, wheelIndex) // 刹车

实现动画。在每帧渲染之前,把车身、车轮的Transom拷贝给three.js或Bayalon.js图形,就实现了动画.

threejs(9)-应用物理引擎设置物体相互作用_第8张图片

实现漂移

在赛车游戏中,漂移能够很大程度上增加游戏的娱乐性。要理解漂移,首先要了解汽车的转向。
后轮无滑移转向时,前轮与后轮的瞬心即为转向中心.
无滑移状态下,转向中心静止不变,车辆将沿转向中心做圆周运动。当漂移时,转向中心也做圆周运动,车辆运动的圆周比无滑移转向时的半径小:
threejs(9)-应用物理引擎设置物体相互作用_第9张图片
实现漂移,可以在用户按下漂移键后,修改后轮的摩擦系数:

vehicle.wheelInfos[2].frictionSlip= up ? 3.5: 1.5;
vehicle.wheelInfos[3].frictionSlip= up ? 3.5: 1.5;

但是,此时很容易出现漂移过度,车子打一个圈:
threejs(9)-应用物理引擎设置物体相互作用_第10张图片
前文中介绍了转向半径,这里再利用一下。设转向半径为r,轴距为l,轮距为w:
threejs(9)-应用物理引擎设置物体相互作用_第11张图片
根据阿克曼条件,两轮的转向角为:

threejs(9)-应用物理引擎设置物体相互作用_第12张图片

r = 6 + Math.abs(vehicle.currentVehicleSpeedKmHour) / 10
        switch (event.keyCode) {
            // 。。。
            case 39: // right
                vehicle.setSteeringValue(up ? 0 : -Math.atan(2 / (r + 1 / 2)), 0);
                vehicle.setSteeringValue(up ? 0 : -Math.atan(2 / (r - 1 / 2)), 1);
                break;

            case 37: // left
                vehicle.setSteeringValue(up ? 0 : Math.atan(2 / (r - 1 / 2)), 0);
                vehicle.setSteeringValue(up ? 0 : Math.atan(2 / (r + 1 / 2)), 1);
                break;

            case 67:
                vehicle.wheelInfos[2].frictionSlip = up ? 3.5 : 1.4;
                vehicle.wheelInfos[3].frictionSlip = up ? 3.5 : 1.4;

threejs(9)-应用物理引擎设置物体相互作用_第13张图片

此外,在卡丁车类游戏中,也可以使用参考文献[1]中,给车身施加侧向力的方法让车辆产生更顺滑的漂移

你可能感兴趣的:(材质,3d)